mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +00:00
Merge branch 'main' into fix/login-crash
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
# OrcaSlicer translations
|
||||
|
||||
The UI strings live in gettext catalogs: `localization/i18n/OrcaSlicer.pot` is the template, and
|
||||
`localization/i18n/<lang>/OrcaSlicer_<lang>.po` holds each language. The `en` catalog is empty on
|
||||
purpose, so skip it. Recurring terms are fixed by the
|
||||
[localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md),
|
||||
whose source is `developer_reference/localization_glossary.tsv` in the **OrcaSlicer_WIKI** repository.
|
||||
A translation pass therefore touches two repositories: the catalogs here, and the glossary there.
|
||||
|
||||
All catalog editing goes through [`scripts/po_tool.py`](scripts/po_tool.py) (standard library only).
|
||||
It lists empty entries with the glossary terms they contain and validates each translation. It writes
|
||||
only the entries it changes, in the layout `msgmerge --no-wrap` produces, and adds the AI comments.
|
||||
Run commands from the repository root, with `python3` (`python` or `py -3` on Windows):
|
||||
|
||||
```bash
|
||||
T=.claude/skills/orca-translations/scripts/po_tool.py
|
||||
python3 $T --help
|
||||
```
|
||||
|
||||
Read [translation-rules.md](references/translation-rules.md) before translating. It holds the
|
||||
mechanical rules, how to pick words, style, and when to flag an entry for review. To find hard-coded
|
||||
English, strings that need a plural form or a context, or files missing from the extraction list, read
|
||||
[source-audit.md](references/source-audit.md); that work changes C++ and comes before a translation pass.
|
||||
|
||||
## Workflow: fill every empty translation
|
||||
|
||||
Unless the user asks for something narrower, a translation pass means the steps below, in order.
|
||||
|
||||
### 1. Find the glossary
|
||||
|
||||
The tool looks for an `OrcaSlicer_WIKI` clone in `--wiki`, then `$ORCA_WIKI_DIR`, then next to this
|
||||
repository.
|
||||
- **A clone is found:** `git -C <wiki> fetch` and make sure it is on an up-to-date `main`. The tool
|
||||
notes when it reads another branch, or a clone that is behind.
|
||||
- **No clone:** the tool reads the published glossary read-only. That is enough to translate, but not
|
||||
to add terms (see step 3).
|
||||
|
||||
### 2. Run the full gettext
|
||||
|
||||
This comes first, always. It regenerates the template from the sources and merges it into every
|
||||
catalog:
|
||||
|
||||
```bash
|
||||
./scripts/run_gettext.sh --full # Linux/macOS: needs gettext and python3 on PATH
|
||||
scripts\run_gettext.bat --full # Windows (PowerShell: & .\scripts\run_gettext.bat --full)
|
||||
```
|
||||
|
||||
It must exit 0. New msgids arrive empty, and removed ones become obsolete `#~` entries. A msgid that
|
||||
comes back gets its old translation revived from `#~`, which is one reason never to purge obsolete
|
||||
entries. It also compiles the `.mo` files (gitignored). The bundled Windows gettext may re-wrap a long
|
||||
`#:` reference line. That churn is harmless.
|
||||
|
||||
Then take stock: `python3 $T status`.
|
||||
|
||||
### 3. Settle new glossary terms before translating anything
|
||||
|
||||
Terms are decided once, for every language, so all the catalogs agree.
|
||||
|
||||
1. `python3 $T candidates` lists recurring phrases in the empty entries that the glossary lacks. Read it
|
||||
together with the new msgids themselves.
|
||||
2. Pick the real terms: feature and mode names, labels that tooltips repeat, and anything translators
|
||||
would otherwise render in several ways.
|
||||
3. Brand names and acronyms belong in the glossary page's hand-written *Kept in English* table instead.
|
||||
4. Skip ordinary words.
|
||||
5. For each term, choose a rendering per language: check `lookup` for existing usage, and follow each
|
||||
language's conventions. Then record it:
|
||||
|
||||
```bash
|
||||
python3 $T glossary-set --term "Belt printer" \
|
||||
--description "One sentence saying what it is, and what it is not." \
|
||||
--set de=... --set es=... --set pt_BR=...
|
||||
```
|
||||
|
||||
The command adds or updates the row, keeps the TSV's BOM and line endings, and regenerates the page.
|
||||
- **An established cell:** it refuses to change it without `--replace`. Changing an established term
|
||||
also means fixing every catalog entry that uses it, so do that only on request.
|
||||
- **An empty cell:** fill it the same way when a pending string establishes the term for that
|
||||
language.
|
||||
|
||||
**No clone, and a new term is needed: stop.** Tell the user the glossary must be updated with these
|
||||
terms, and list each one with its description and proposed renderings (`glossary-set` prints them too).
|
||||
Ask them to clone `https://github.com/OrcaSlicer/OrcaSlicer_WIKI` next to this repository, or to give
|
||||
its path. Translate nothing that uses those terms until they answer.
|
||||
|
||||
### 4. Translate, language by language, in batches
|
||||
|
||||
For each language:
|
||||
|
||||
1. Read its glossary column and notes once: `python3 $T glossary --lang <L>`.
|
||||
2. Loop over the empty entries:
|
||||
|
||||
```bash
|
||||
python3 $T pending --lang <L> --limit 60 --out <scratch>/<L>.json
|
||||
# write <scratch>/<L>-tr.json: [{"id": "...", "msgstr": "..."}, ...], "msgstr_plural" for plurals,
|
||||
# plus "review": "<what to check>" where unsure
|
||||
python3 $T apply --lang <L> --input <scratch>/<L>-tr.json
|
||||
```
|
||||
|
||||
- **Batch files go in a scratch directory, not the repository.**
|
||||
- **Each entry's `id` identifies it,** so the translation file need not repeat the English. `apply`
|
||||
also accepts `msgid` with `msgctxt`, or the pending file itself with its `msgstr` fields filled.
|
||||
- **Translate every entry in the batch.** A blank `msgstr` is skipped, not cleared.
|
||||
- **Next batch:** applied entries leave the list, so run `pending` again at offset 0. Use `--offset`
|
||||
only to step past entries you deliberately leave blank.
|
||||
- **What each entry carries:**
|
||||
- `glossary`: the terms it contains, with the language's rendering.
|
||||
- `note`: the developer's `TRN` comment.
|
||||
- `flags`: `c-format` or `boost-format`.
|
||||
- `description`: present when a sense has to be chosen.
|
||||
3. Read every apply message:
|
||||
- **REJECTED:** a hard rule broke. Fix the translation and apply the corrected items again.
|
||||
- **WARNING:** something worth a second look, such as a glossary rendering not found, ending
|
||||
punctuation, or snake_case. A glossary warning on an inflected form you checked is fine.
|
||||
Otherwise correct the translation (`apply --overwrite` replaces your own entry), or add a
|
||||
`review` reason.
|
||||
|
||||
`apply` adds `# AI Translated` to every entry it writes, and `# Needs human review: <reason>` where an
|
||||
item has `review`. On an `--overwrite`, `"review": ""` removes an earlier note. Never add either to a translation a person wrote. `--human` is for applying their
|
||||
text.
|
||||
|
||||
**The work is large:** every new string, times every language. When subagents are available, give each
|
||||
one a language or a few. Each agent only touches its own catalogs, so they can run
|
||||
in parallel. Settle step 3 first. Give each agent the language list, this skill's path, the rules file,
|
||||
and these instructions:
|
||||
- Fill every empty entry.
|
||||
- Don't add glossary rows.
|
||||
- Report what it could not translate, the review notes it added, and any term it thinks the glossary
|
||||
still lacks.
|
||||
|
||||
Add the terms they report yourself, then have the affected languages revisit the entries.
|
||||
|
||||
### 5. Verify
|
||||
|
||||
```bash
|
||||
python3 $T check --lang <L> --warnings # every AI-translated entry, plus msgfmt --check-format
|
||||
```
|
||||
|
||||
- **Run the full gettext again.** It must exit 0 and leave the catalogs as the pass left them.
|
||||
- **Run `python generate_glossary.py --check` in the wiki clone.**
|
||||
- **Errors on entries you didn't write predate the pass.** Report them rather than fixing them in the
|
||||
same change.
|
||||
|
||||
### 6. Report
|
||||
|
||||
Report:
|
||||
- the entries filled, per language;
|
||||
- the review notes added;
|
||||
- anything left empty, and why;
|
||||
- the glossary rows added or filled.
|
||||
|
||||
The catalogs (and `OrcaSlicer.pot`) and the glossary (`localization_glossary.tsv` and `.md`) are
|
||||
separate changes in separate repositories. The wiki change should land with, or before, the catalog
|
||||
change. Commit or open PRs only when asked.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Run the full gettext first.** Translate against the regenerated template, never a stale one.
|
||||
2. **The glossary is binding.** Use the language's rendering for each term, in the right sense. When a
|
||||
rendering looks wrong, use it and flag the entry. Then raise the change with the user, rather than
|
||||
deviating in one string.
|
||||
3. **New recurring terms go into the glossary before the translations that use them.** Without a wiki
|
||||
clone, stop and tell the user (step 3).
|
||||
4. **Fill, don't rewrite.** A fill pass writes only empty and fuzzy entries.
|
||||
- Existing translations, human or AI, change only on request (`--overwrite`).
|
||||
- A glossary violation in a human translation is reported, not changed.
|
||||
5. **Machine output is labelled.** Every AI translation gets `# AI Translated`; uncertain ones also get
|
||||
`# Needs human review: <reason>`. Never use `fuzzy`.
|
||||
6. **Never edit `msgid`, `msgctxt`, the header, or obsolete `#~` entries,** and never fix the English in
|
||||
a translation. Report source problems (see the rules file).
|
||||
7. **Write catalogs only through `po_tool.py` or gettext.** PO libraries such as polib reflow unrelated
|
||||
entries, and hand edits skip the validation.
|
||||
|
||||
## Changing an established term
|
||||
|
||||
A glossary cell is usually copied from the catalog's own label. When the label is wrong, the glossary
|
||||
inherits the mistake, so fix both in the same change:
|
||||
|
||||
1. **Check the catalog's usage first.** `lookup --lang <L> "<term>"` searches the English, and with
|
||||
`--msgstr` it searches the translations.
|
||||
- When most entries already use the correct form, move the cell to it.
|
||||
- When the wrong form is used consistently throughout, leave both as they are and ask a
|
||||
translator.
|
||||
2. **Update the cell:** `glossary-set --term "<Term>" --replace --set <L>=<value>`.
|
||||
3. **Rewrite the entries that still use the old form,** only where the term has that meaning.
|
||||
- Write `[{"id": "<id from lookup>", "msgstr": "<full corrected text>"}]` and run
|
||||
`apply --overwrite`.
|
||||
- The rewritten entries are marked `# AI Translated`.
|
||||
|
||||
## Reviewing a translation change
|
||||
|
||||
Review it for these:
|
||||
- glossary compliance (`check --scope all --warnings` on the language, then read the hits);
|
||||
- placeholders and line breaks (`check` errors);
|
||||
- `# AI Translated` kept on text a person rewrote, or added to text a person wrote;
|
||||
- purged obsolete entries;
|
||||
- reflowed unrelated entries;
|
||||
- plural forms short of `nplurals`.
|
||||
|
||||
When a reviewer disputes a term, compare it with the glossary first. A machine translation that follows
|
||||
the glossary is correct even if the term itself is debatable. Then the fix is a glossary change, made
|
||||
with the translators, followed by the catalogs.
|
||||
|
||||
## Source of truth
|
||||
|
||||
When this skill and the checkouts disagree, the checkouts win:
|
||||
- `scripts/run_gettext.sh` / `.bat` for extraction and merging;
|
||||
- `localization/i18n/list.txt` for the scanned sources;
|
||||
- the wiki's `localization_guide.md` and `localization_glossary.md` / `.tsv` for the rules and terms;
|
||||
- `AGENTS.md` (Localization & translations) for the repository's conventions.
|
||||
|
||||
## Editing this skill
|
||||
|
||||
State rules and procedures, not the state of the catalogs: no counts, lists of untranslated strings or
|
||||
per-language defects. Language-specific terminology belongs in the glossary's *Language specifics*
|
||||
section, not here.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Auditing the source for translatable text
|
||||
|
||||
Use this when asked to find hard-coded English, strings that need a context, or strings that need plural
|
||||
forms. It changes C++ and the `.pot`, so it runs before a translation pass and is followed by one.
|
||||
|
||||
## How a string reaches translators
|
||||
|
||||
- `xgettext` reads only the files named in `localization/i18n/list.txt`, and only the macros
|
||||
`L`, `_L`, `_u8L`, `L_CONTEXT`, `_L_CONTEXT`, `_u8L_CONTEXT` and `_L_PLURAL`.
|
||||
- A file that marks strings but is missing from `list.txt` is silently never extracted. Find such
|
||||
files by comparing `grep -l` for the macros with the list.
|
||||
- `_()`, `_utf8()` and `wxGetTranslation()` do not extract. Calls that rely on them need an `L()`
|
||||
or `_L()` somewhere xgettext sees.
|
||||
- **GUI code:** `_L` gives a `wxString`, `_u8L` a UTF-8 `std::string` (use it for ImGui). `L` only
|
||||
marks the string; the display site must translate it.
|
||||
- **libslic3r:** the macro is whatever the file defines. Some files `#define L(s)
|
||||
Slic3r::I18N::translate(s)`, so `L()` translates at runtime there. Others, such as `PrintConfig.cpp`,
|
||||
define it as a no-op marker, and the GUI translates the text later. Read the top of the file before
|
||||
judging a `format(L(...))` call. libslic3r has no plural or context lookup at runtime.
|
||||
- The libslic3r translate callback is installed in `GUI_App::on_init_inner`. Text produced before it
|
||||
is installed stays in English.
|
||||
|
||||
## What to look for
|
||||
|
||||
1. **Visible English with no macro:** labels, tooltips, menu items, dialog text, notifications and
|
||||
ImGui text. `scripts/find_untranslated.py OUT_DIR` lists candidates; most are noise, so trace each
|
||||
one to the place it is shown.
|
||||
2. **Sentences built from translated pieces:** `_L("A") + name + _L(" is B")`, or a translated word
|
||||
spliced into another sentence. Translators cannot reorder or inflect these. Replace each with one
|
||||
msgid with placeholders. A spliced fragment with a leading or trailing space is the usual sign.
|
||||
3. **Counts next to words that must agree:** `(s)`, "%d objects", a verb that follows a number. Use
|
||||
`_L_PLURAL(singular, plural, n)`. If the singular and plural are identical on purpose, translators
|
||||
still inflect each form.
|
||||
4. **One English word with several meanings:** a short msgid used in files with different meanings
|
||||
(a verb in one place, a noun in another, a mode name, a CAD term). Generate locations with
|
||||
`xgettext` *without* `--no-location` and list short msgids that occur in two or more files.
|
||||
5. **Code that compares translated text with English,** or decides behaviour from a message text.
|
||||
Translation breaks it. Compare ids or flags instead.
|
||||
6. **Tests that quote message text:** `grep` the tests for a message before rewording it.
|
||||
|
||||
## What not to translate
|
||||
|
||||
Keys, config values and identifiers; log and debug output; assertion and check messages; developer-only
|
||||
UI behind debug switches; undo/redo snapshot names (the history is never shown); text returned to a
|
||||
plugin's code; HTML, JS and desktop-file content in string literals; brand, printer-model and format
|
||||
names.
|
||||
|
||||
## Adding a context
|
||||
|
||||
- Keep the most common sense without a context, so its existing translations stay valid. Give the
|
||||
other senses a short, readable context.
|
||||
- A context cannot be carried through code that translates generically: enum labels that
|
||||
`Field.cpp` translates with `_()`, tables translated by a loop, and anything in libslic3r. Leave that
|
||||
sense on the plain entry and give the contexts to the other uses.
|
||||
|
||||
## After the edits
|
||||
|
||||
1. Build and run the tests (`libslic3r_tests` in particular).
|
||||
2. Run `run_gettext --full`. Reworded strings become new empty entries and the old ones become
|
||||
obsolete, which keeps their translations as a memory. Strings that gain a plural form leave a fuzzy
|
||||
entry.
|
||||
3. Run the translation pass from SKILL.md. `po_tool.py pending` suggests the closest earlier translation
|
||||
for each entry.
|
||||
4. Fix English typos *before* the translation pass; fixing one afterwards orphans every translation.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Translation rules
|
||||
|
||||
The rules a translation must follow, condensed from the wiki's
|
||||
[Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md),
|
||||
the [glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md)
|
||||
and the localization section of `AGENTS.md`. `po_tool.py apply` rejects the mechanical violations it
|
||||
can detect; everything else is on the translator.
|
||||
|
||||
## Mechanical: enforced by `apply`
|
||||
|
||||
A rejected entry is not written. Fix it and apply again.
|
||||
|
||||
- **Placeholders stay exactly as written:** `%s`, `%d`, `%.1f`, `%1$d`, `%1%`, `%%`. Never `%1 %`, never
|
||||
`%` for `%%`. Plain printf placeholders keep their order; only positional ones (`%1$s`, `%1%`) may move.
|
||||
A broken placeholder crashes the app.
|
||||
- **Line breaks:** the same number of `\n`, with the same leading and trailing ones. Don't add breaks to fit
|
||||
text, least of all in parameter names.
|
||||
- **Leading and trailing spaces** are kept. A leading space usually means the string is appended to another.
|
||||
- **HTML tags** are kept, and the text between them is translated: `Click <a>here</a> to install it.`
|
||||
- **`℃`** (U+2103) stays one character, written exactly as in the source: `%d℃`, never `°C`, `° C` or
|
||||
`%d ℃`.
|
||||
- **Tokens stay verbatim:** `{placeholders}`, `[macros]`, G-code commands (`M141`, `G28`). Setting keys
|
||||
in snake_case (`use_relative_e_distances`) are flagged as warnings, because a few snake_case msgids
|
||||
are really labels (`top_front`). Keep a key unless the string uses it as a display name.
|
||||
- **Plural entries** need every form from the catalog's `Plural-Forms` header (`nplurals`: 1 for
|
||||
ja/ko/zh/th/vi, 3 for ru/cs/pl/lt, 4 for uk). Each form is really inflected for its quantity. Repeating
|
||||
one sentence is a bug in Slavic and Baltic languages, but correct in Turkish and Hungarian.
|
||||
|
||||
## Mechanical: not detectable, still required
|
||||
|
||||
- Edit only `msgstr`. Never change `msgid`, `msgctxt`, the header or another entry's text.
|
||||
- **Ending punctuation follows the source:** no added period, and no dropped one. `apply` warns.
|
||||
- Units follow the SI convention of the language: `s`, not `sec`. A parameter description holds no
|
||||
unit, because the field shows it.
|
||||
- **A button or short label holds one translation:** no alternatives in parentheses. They widen the
|
||||
control.
|
||||
- Don't use `fuzzy` for uncertainty. Fuzzy entries are hidden, and users then see English.
|
||||
|
||||
## Choosing the words
|
||||
|
||||
- **The glossary is binding.** Use the language's rendering for every glossary term the `pending`
|
||||
output lists, inflected as the sentence needs.
|
||||
- **Rows that share a base term are different senses:** `Seam` (the print seam) versus `Seam (UV)`
|
||||
(the unwrap cut), and `Instance` versus `Instance (application)`. Pick by the description.
|
||||
- **A qualified row may be a false match.** `Feature (Design)` does not apply to "features of the
|
||||
app".
|
||||
- **A cell marked ⚠ (or holding two renderings) is unsettled.** Use the form the catalog already
|
||||
uses for that sense (`lookup`), and flag the entry for review.
|
||||
- **Kept in English:** brand and product names (OrcaSlicer, Bambu Lab, Orca Cloud, AMS, Micro-Lidar),
|
||||
acronyms (FDM, LAN, HTTPS), file formats, material codes (PLA, PETG-CF), G-code, macros and variables
|
||||
stay as written. In "Adaptive Pressure Advance", only "Pressure Advance" stays English. See the
|
||||
glossary page's *Kept in English* table.
|
||||
- **Language specifics** on the glossary page (`po_tool.py glossary --lang L` prints them) override
|
||||
general habits for that language.
|
||||
- **Consistency beyond the glossary:** before translating a label, or a tooltip that names a setting,
|
||||
run `lookup` for the setting name. Quote the label's existing translation exactly, so the tooltip and
|
||||
the field match. Reuse obsolete (`#~`) entries as a translation memory.
|
||||
- **`msgctxt` separates homonyms.** `Back` in *Camera View* is the rear view, and `Back` in
|
||||
*Navigation* is the go-back button. Translate each context on its own.
|
||||
- **Overloaded English: translate the meaning.**
|
||||
- *Flow ratio* is a multiplier, *Flow Rate* is throughput, and *Flow Dynamics* is pressure
|
||||
compensation. These are three different terms.
|
||||
- *Extruder* can mean the toolhead, the feeder motor ("High extruder current on filament swap") or
|
||||
the nozzle ("all extruders must have the same diameter").
|
||||
- When a short label is ambiguous, grep `src/` for the msgid to see the widget it labels.
|
||||
- **Boilerplate stays uniform.** `Failed to connect to …`, `Are you sure you want to …?` and
|
||||
`If expressed as a %, it will be computed over …` each get one template in the language, even where
|
||||
the English varies.
|
||||
|
||||
## Style
|
||||
|
||||
- **Localize, don't translate word-for-word.** Know what the string controls before translating it.
|
||||
- **Simplify developer phrasing** when the meaning survives. *"This option determines whether
|
||||
OrcaSlicer should apply the retract reduction on infill"* reads better as *"Disable retractions while
|
||||
printing infill."* Introduce nuances in following sentences, not in one packed clause.
|
||||
- **Don't expand.** A machine translation adds no facts the English doesn't state. Expanding
|
||||
descriptions is for human translators, who can confirm the extra content.
|
||||
- **Write for both audiences:** professionals and people with a printer at home. Keep the established
|
||||
terminology, used as plainly as the string allows.
|
||||
|
||||
## When to add a review note
|
||||
|
||||
Add `"review": "<what to check>"` to an item when the translation is probable but unconfirmed. It
|
||||
becomes `# Needs human review: …`, and the entry still ships. Typical reasons:
|
||||
|
||||
- The English is ambiguous out of context: "Tile" as a verb or a noun.
|
||||
- A short label's role only shows in the app.
|
||||
- Two established terms compete, or the glossary cell is unsettled.
|
||||
- The string's grammar depends on a value substituted at runtime: `%zu %s` counts with no plural entry.
|
||||
|
||||
Say exactly what to look at and where: *"Row name in the Design tab tool-offer menu; check it reads
|
||||
right for the whole row."*
|
||||
|
||||
## Problems that belong in the source, not the translation
|
||||
|
||||
Report these; don't work around them in a catalog.
|
||||
|
||||
- **The English is wrong.** Never "fix" it in the translation only.
|
||||
- **One msgid needs two translations.** The source needs `_L_CONTEXT` / `_u8L_CONTEXT` (a code change).
|
||||
- **A literal `%` in a string xgettext flagged `possible-c-format`** fails `msgfmt`. The fix is a
|
||||
`// xgettext:no-c-format, no-boost-format` comment above the string in the source, not `%%` in the
|
||||
translation.
|
||||
@@ -0,0 +1,113 @@
|
||||
"""List C++ string literals that look like user-facing English but are not marked for translation.
|
||||
|
||||
Usage: find_untranslated.py OUT_DIR (one hits file per source area; each line is file:line: [call] "text" | code)
|
||||
Works on whole files, so literals continuing a translation macro call from an earlier line are
|
||||
recognised. Skips comments, log/assert/check calls, includes, printer model lists and markup."""
|
||||
import re, sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(sys.argv[1]); OUT.mkdir(parents=True, exist_ok=True)
|
||||
MACROS = {"_L", "_u8L", "L", "_L_CONTEXT", "_u8L_CONTEXT", "L_CONTEXT", "_L_PLURAL", "_devL", "_omitL",
|
||||
"_CHB", "_utf8", "_", "translate", "translate_utf8", "wxGetTranslation", "T"}
|
||||
NOISE_CALLS = {"BOOST_LOG_TRIVIAL", "wxCHECK", "wxCHECK_MSG", "wxCHECK_RET", "wxASSERT", "wxASSERT_MSG", "wxFAIL_MSG",
|
||||
"assert", "static_assert", "printf", "fprintf", "sprintf", "snprintf", "wxLogDebug", "wxLogTrace",
|
||||
"wxLogMessage", "wxLogError", "wxLogWarning", "OutputDebugStringA", "OutputDebugStringW", "DEBUG_LOG",
|
||||
"glsafe", "GLShaderProgram", "find", "count", "contains", "starts_with", "ends_with", "get_value",
|
||||
"has", "opt_bool", "opt_int", "opt_float", "opt_string", "option", "get", "set_key_value", "at",
|
||||
"boost_log", "LOGD", "LOGE", "LOGI", "LOGW", "SPDLOG", "CLI_LOG"}
|
||||
LIT = re.compile(r'"((?:[^"\\\n]|\\.)*)"')
|
||||
WORD = re.compile(r"[A-Za-z]{2,}")
|
||||
|
||||
def strip_comments(text):
|
||||
# Replace comments with spaces, keeping offsets and string literals intact.
|
||||
out, i, n = [], 0, len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '"' or c == "'":
|
||||
j = i + 1
|
||||
while j < n and text[j] != c and text[j] != "\n":
|
||||
j += 2 if text[j] == "\\" else 1
|
||||
out.append(text[i:j + 1]); i = j + 1
|
||||
elif text.startswith("//", i):
|
||||
j = text.find("\n", i); j = n if j < 0 else j
|
||||
out.append(" " * (j - i)); i = j
|
||||
elif text.startswith("/*", i):
|
||||
j = text.find("*/", i + 2); j = n if j < 0 else j + 2
|
||||
out.append(re.sub(r"[^\n]", " ", text[i:j])); i = j
|
||||
else:
|
||||
out.append(c); i += 1
|
||||
return "".join(out)
|
||||
|
||||
def enclosing_call(text, pos):
|
||||
depth = 0
|
||||
i = pos - 1
|
||||
while i >= 0:
|
||||
c = text[i]
|
||||
if c == '"': # skip over a literal backwards
|
||||
i -= 1
|
||||
while i >= 0 and not (text[i] == '"' and text[i - 1] != "\\"):
|
||||
i -= 1
|
||||
elif c in ")]}":
|
||||
depth += 1
|
||||
elif c in "([{":
|
||||
if depth == 0:
|
||||
if c != "(":
|
||||
return None
|
||||
m = re.search(r"([A-Za-z_][\w:]*)\s*$", text[max(0, i - 80):i])
|
||||
return m.group(1).split("::")[-1] if m else None
|
||||
depth -= 1
|
||||
elif c == ";" and depth == 0:
|
||||
return None
|
||||
i -= 1
|
||||
return None
|
||||
|
||||
def looks_user_facing(s):
|
||||
if len(WORD.findall(s)) < 2 and not re.fullmatch(r"[A-Z][a-z]{2,}[.!?:]?", s):
|
||||
return False
|
||||
if " " not in s and re.fullmatch(r"[\w./\\:#%-]+", s):
|
||||
return False
|
||||
if re.search(r"https?://|<\w+[^>]*>|\.(png|svg|json|ini|3mf|stl|gcode|bbl|js|html|css|txt|xml|zip)\b|=\s*\w+;|^\s*[-\w]+\s*:\s*[\w#]", s):
|
||||
return False
|
||||
if re.fullmatch(r"[a-z0-9_ ]+", s) and "_" in s:
|
||||
return False
|
||||
return bool(re.search(r"[A-Za-z]{2,} [a-z]{2,}|^[A-Z][a-z]{2,}", s))
|
||||
|
||||
groups = {}
|
||||
for path in sorted((REPO / "src").rglob("*.[ch]pp")):
|
||||
rel = path.relative_to(REPO).as_posix()
|
||||
if "/dev-utils/" in rel or "/libvgcode/" in rel or rel.startswith("src/libnest2d") or "/tests/" in rel:
|
||||
continue
|
||||
raw = path.read_text(encoding="utf-8", errors="replace")
|
||||
text = strip_comments(raw)
|
||||
line_starts = [0] + [m.end() for m in re.finditer("\n", text)]
|
||||
lines = raw.splitlines()
|
||||
hits = []
|
||||
for m in LIT.finditer(text):
|
||||
s = m.group(1)
|
||||
if not looks_user_facing(s):
|
||||
continue
|
||||
call = enclosing_call(text, m.start())
|
||||
if call in MACROS or call in NOISE_CALLS:
|
||||
continue
|
||||
ln = next(k for k in range(len(line_starts) - 1, -1, -1) if line_starts[k] <= m.start()) + 1
|
||||
src = lines[ln - 1].strip()
|
||||
if re.search(r"BOOST_LOG|#\s*include|<<\s*\"|\"\s*<<|std::cerr|std::cout", src):
|
||||
continue
|
||||
# printer model / vendor tables
|
||||
if len(LIT.findall(src)) >= 4 and not re.search(r"_L|_u8L", src):
|
||||
continue
|
||||
hits.append(f"{rel}:{ln}: [{call or '-'}] \"{s[:150]}\" | {src[:170]}")
|
||||
if hits:
|
||||
parts = rel.split("/")
|
||||
if parts[1] == "slic3r":
|
||||
key = f"GUI/{parts[3] if len(parts) > 4 else 'root'}" if parts[2] == "GUI" else f"slic3r/{parts[2]}"
|
||||
else:
|
||||
key = parts[1].replace(".cpp", "")
|
||||
groups.setdefault(key, []).extend(hits)
|
||||
total = 0
|
||||
for key, hits in sorted(groups.items()):
|
||||
(OUT / (key.replace("/", "_") + ".txt")).write_text("\n".join(hits) + "\n", encoding="utf-8")
|
||||
total += len(hits)
|
||||
print(f"{len(hits):5} {key}")
|
||||
print("total", total)
|
||||
@@ -0,0 +1,870 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Translation helper for the OrcaSlicer gettext catalogs and the wiki glossary.
|
||||
|
||||
Standard library only. Edits a catalog in place, rewriting just the entries it
|
||||
changes in the layout `msgmerge --no-wrap` writes, so every other byte stays put.
|
||||
|
||||
status [--lang L ...] per-catalog counts
|
||||
pending --lang L [--offset N --limit N] [--out FILE] [--no-include-fuzzy] (adds similar earlier translations)
|
||||
apply --lang L --input FILE [--overwrite] [--dry-run] adds "# AI Translated"
|
||||
check [--lang L ...] [--scope ai|all] [--warnings]
|
||||
lookup --lang L PATTERN [--msgstr] existing translations with ids, obsolete ones included
|
||||
glossary --lang L [TERM] the language's glossary column and notes
|
||||
candidates [--min-count N] [--limit N] recurring untranslated phrases not in the glossary
|
||||
glossary-set --term T [--description D] --set L=VALUE ... [--replace]
|
||||
|
||||
The glossary is read from an OrcaSlicer_WIKI clone (--wiki, $ORCA_WIKI_DIR, or a
|
||||
sibling of this repository). Without one it is downloaded read-only, and
|
||||
glossary-set refuses to run.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
I18N = REPO / "localization" / "i18n"
|
||||
SKIP_LANGS = {"en"} # the English catalog is intentionally empty
|
||||
GLOSSARY_TSV = Path("developer_reference") / "localization_glossary.tsv"
|
||||
GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md"
|
||||
MISSING = {"", "—"}
|
||||
RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/"
|
||||
AI_TAG = "AI Translated"
|
||||
REVIEW_TAG = "Needs human review:"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PO parsing and writing
|
||||
|
||||
_UNESCAPE = {"n": "\n", "t": "\t", "r": "\r", "a": "\a", "b": "\b", "f": "\f", "v": "\v", '"': '"', "\\": "\\"}
|
||||
_ESCAPE = {v: "\\" + k for k, v in _UNESCAPE.items()}
|
||||
|
||||
|
||||
def unescape(s):
|
||||
return re.sub(r'\\(.)', lambda m: _UNESCAPE.get(m.group(1), m.group(0)), s)
|
||||
|
||||
|
||||
def escape(s):
|
||||
return "".join(_ESCAPE.get(c, c) for c in s)
|
||||
|
||||
|
||||
def format_field(keyword, value, prefix=""):
|
||||
"""Lines for `keyword "value"` as msgmerge --no-wrap writes them."""
|
||||
parts = value.split("\n")
|
||||
segments = [p + "\n" for p in parts[:-1]] + ([parts[-1]] if parts[-1] else [])
|
||||
if len(segments) <= 1:
|
||||
return [f'{prefix}{keyword} "{escape(value)}"']
|
||||
return [f'{prefix}{keyword} ""'] + [f'{prefix}"{escape(s)}"' for s in segments]
|
||||
|
||||
|
||||
class Entry:
|
||||
"""One blank-line-delimited block of a PO file."""
|
||||
|
||||
def __init__(self, lines, first_line):
|
||||
self.lines = lines
|
||||
self.first_line = first_line # 0-based index in the file
|
||||
self.obsolete = all(l.startswith("#~") for l in lines if not l.startswith("#") or l.startswith("#~"))
|
||||
self.tcomments, self.xcomments, self.flags = [], [], []
|
||||
self.fields = {} # keyword -> [value, start, end)
|
||||
self.order = []
|
||||
current = None
|
||||
for i, raw in enumerate(lines):
|
||||
line = raw
|
||||
if self.obsolete and line.startswith("#~"):
|
||||
line = line[2:].lstrip(" ")
|
||||
if line.startswith("|"):
|
||||
current = None
|
||||
continue
|
||||
elif line.startswith("#"):
|
||||
current = None
|
||||
if line.startswith("#."):
|
||||
self.xcomments.append(line[2:].strip())
|
||||
elif line.startswith("#,"):
|
||||
self.flags += [f.strip() for f in line[2:].split(",") if f.strip()]
|
||||
elif line == "#" or line.startswith("# "):
|
||||
self.tcomments.append(line[2:])
|
||||
continue
|
||||
m = re.match(r'^(msgctxt|msgid_plural|msgid|msgstr(?:\[\d+\])?)\s+"(.*)"\s*$', line)
|
||||
if m:
|
||||
current = m.group(1)
|
||||
self.fields[current] = [unescape(m.group(2)), i, i + 1]
|
||||
self.order.append(current)
|
||||
elif line.startswith('"') and current:
|
||||
self.fields[current][0] += unescape(line.strip()[1:-1])
|
||||
self.fields[current][2] = i + 1
|
||||
|
||||
def get(self, key, default=None):
|
||||
f = self.fields.get(key)
|
||||
return f[0] if f else default
|
||||
|
||||
@property
|
||||
def msgctxt(self):
|
||||
return self.get("msgctxt")
|
||||
|
||||
@property
|
||||
def msgid(self):
|
||||
return self.get("msgid", "")
|
||||
|
||||
@property
|
||||
def msgid_plural(self):
|
||||
return self.get("msgid_plural")
|
||||
|
||||
@property
|
||||
def is_header(self):
|
||||
return self.msgid == "" and "msgid" in self.fields and not self.msgctxt
|
||||
|
||||
def msgstrs(self):
|
||||
if self.msgid_plural is None:
|
||||
return [self.get("msgstr", "")]
|
||||
idx = sorted(int(k[7:-1]) for k in self.fields if k.startswith("msgstr["))
|
||||
return [self.get(f"msgstr[{i}]", "") for i in range(max(idx) + 1)] if idx else []
|
||||
|
||||
@property
|
||||
def fuzzy(self):
|
||||
return "fuzzy" in self.flags
|
||||
|
||||
@property
|
||||
def ai(self):
|
||||
return any(c.strip() == AI_TAG for c in self.tcomments)
|
||||
|
||||
@property
|
||||
def review(self):
|
||||
return [c for c in self.tcomments if c.startswith(REVIEW_TAG)]
|
||||
|
||||
def key(self):
|
||||
return (self.msgctxt, self.msgid)
|
||||
|
||||
def id(self):
|
||||
"""Short stable handle, so a translation file need not repeat the English."""
|
||||
return hashlib.sha1(f"{self.msgctxt or ''}\x04{self.msgid}".encode("utf-8")).hexdigest()[:10]
|
||||
|
||||
def rewrite(self, msgstrs, ai=True, review=None):
|
||||
"""Replace the translation; add the AI and review comments; clear fuzzy."""
|
||||
lines = list(self.lines)
|
||||
# translations
|
||||
keys = [k for k in self.order if k.startswith("msgstr")]
|
||||
start = min(self.fields[k][1] for k in keys)
|
||||
end = max(self.fields[k][2] for k in keys)
|
||||
if self.msgid_plural is None:
|
||||
new = format_field("msgstr", msgstrs[0])
|
||||
else:
|
||||
new = [l for i, s in enumerate(msgstrs) for l in format_field(f"msgstr[{i}]", s)]
|
||||
lines[start:end] = new
|
||||
# flags and previous-msgid lines of a fuzzy entry
|
||||
out = []
|
||||
for line in lines:
|
||||
if line.startswith("#,"):
|
||||
flags = [f.strip() for f in line[2:].split(",") if f.strip() and f.strip() != "fuzzy"]
|
||||
if flags:
|
||||
out.append("#, " + ", ".join(flags))
|
||||
continue
|
||||
if line.startswith("#|") and self.fuzzy:
|
||||
continue
|
||||
out.append(line)
|
||||
lines = out
|
||||
# translator comments go first, before #. #: #,
|
||||
pos = 0
|
||||
while pos < len(lines) and (lines[pos] == "#" or lines[pos].startswith("# ")):
|
||||
pos += 1
|
||||
add = []
|
||||
if review is not None: # "" clears an earlier note, a reason replaces it
|
||||
lines = [l for l in lines if not l.startswith("# " + REVIEW_TAG)]
|
||||
pos = 0
|
||||
while pos < len(lines) and (lines[pos] == "#" or lines[pos].startswith("# ")):
|
||||
pos += 1
|
||||
if ai and not any(l == "# " + AI_TAG for l in lines[:pos]):
|
||||
add.append("# " + AI_TAG)
|
||||
if review:
|
||||
add.append(f"# {REVIEW_TAG} {' '.join(review.split())}")
|
||||
lines[pos:pos] = add
|
||||
return lines
|
||||
|
||||
|
||||
class Catalog:
|
||||
def __init__(self, path):
|
||||
self.path = Path(path)
|
||||
data = self.path.read_bytes()
|
||||
self.bom = data.startswith(b"\xef\xbb\xbf")
|
||||
text = data.decode("utf-8-sig")
|
||||
self.newline = "\r\n" if "\r\n" in text else "\n"
|
||||
self.trailing = text.endswith(("\n", "\r"))
|
||||
self.lines = text.splitlines()
|
||||
self.entries = []
|
||||
block, first = [], 0
|
||||
for i, line in enumerate(self.lines + [""]):
|
||||
if line.strip() == "":
|
||||
if block:
|
||||
self.entries.append(Entry(block, first))
|
||||
block = []
|
||||
else:
|
||||
if not block:
|
||||
first = i
|
||||
block.append(line)
|
||||
self.header = next((e for e in self.entries if e.is_header), None)
|
||||
m = re.search(r"nplurals\s*=\s*(\d+)", self.header.msgstrs()[0] if self.header else "")
|
||||
self.nplurals = int(m.group(1)) if m else 2
|
||||
self.changes = {} # entry index -> new lines
|
||||
|
||||
def active(self):
|
||||
return [e for e in self.entries if not e.obsolete and not e.is_header]
|
||||
|
||||
def index(self):
|
||||
return {e.key(): i for i, e in enumerate(self.entries) if not e.obsolete and not e.is_header}
|
||||
|
||||
def save(self):
|
||||
lines = list(self.lines)
|
||||
for i in sorted(self.changes, reverse=True):
|
||||
e = self.entries[i]
|
||||
lines[e.first_line:e.first_line + len(e.lines)] = self.changes[i]
|
||||
text = self.newline.join(lines) + (self.newline if self.trailing else "")
|
||||
self.path.write_bytes((b"\xef\xbb\xbf" if self.bom else b"") + text.encode("utf-8"))
|
||||
|
||||
|
||||
def needs_translation(e, nplurals, include_fuzzy=False):
|
||||
strs = e.msgstrs()
|
||||
if e.msgid_plural is not None and len(strs) < nplurals:
|
||||
return True
|
||||
return any(s == "" for s in strs) or (include_fuzzy and e.fuzzy)
|
||||
|
||||
|
||||
def catalog_path(lang):
|
||||
p = I18N / lang / f"OrcaSlicer_{lang}.po"
|
||||
if not p.exists():
|
||||
sys.exit(f"no catalog for '{lang}': {p}")
|
||||
return p
|
||||
|
||||
|
||||
def all_langs():
|
||||
return sorted(d.name for d in I18N.iterdir() if (d / f"OrcaSlicer_{d.name}.po").exists() and d.name not in SKIP_LANGS)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Glossary
|
||||
|
||||
def find_wiki(arg):
|
||||
for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]:
|
||||
if cand and (Path(cand) / GLOSSARY_TSV).exists():
|
||||
return Path(cand)
|
||||
return None
|
||||
|
||||
|
||||
def _download(rel):
|
||||
cache = Path(tempfile.gettempdir()) / ("orca_glossary_" + rel.name)
|
||||
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:
|
||||
cache.write_bytes(r.read())
|
||||
return cache
|
||||
|
||||
|
||||
class Glossary:
|
||||
def __init__(self, wiki_arg=None, quiet=False):
|
||||
self.wiki = find_wiki(wiki_arg)
|
||||
if self.wiki:
|
||||
self.tsv, self.md = self.wiki / GLOSSARY_TSV, self.wiki / GLOSSARY_MD
|
||||
if not quiet:
|
||||
self.report_clone_state()
|
||||
else:
|
||||
if not quiet:
|
||||
print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr)
|
||||
self.tsv, self.md = _download(GLOSSARY_TSV), _download(GLOSSARY_MD)
|
||||
self.load()
|
||||
|
||||
def report_clone_state(self):
|
||||
git = lambda *args: subprocess.run(["git", "-C", str(self.wiki), *args], capture_output=True, text=True).stdout.strip()
|
||||
try:
|
||||
branch = git("rev-parse", "--abbrev-ref", "HEAD")
|
||||
behind = git("rev-list", "--count", "HEAD..origin/main")
|
||||
except OSError:
|
||||
return
|
||||
if branch and branch != "main":
|
||||
print(f"note: glossary read from wiki branch '{branch}', not main", file=sys.stderr)
|
||||
if behind and behind != "0":
|
||||
print(f"note: the wiki clone is {behind} commits behind origin/main (as of its last fetch)", file=sys.stderr)
|
||||
|
||||
def load(self):
|
||||
rows = [l.split("\t") for l in self.tsv.read_text(encoding="utf-8-sig").splitlines() if l.strip()]
|
||||
self.header = rows[0]
|
||||
# "—" marks a language with no established rendering yet
|
||||
self.rows = [[c if c.strip() not in MISSING else "" for c in r] + [""] * (len(rows[0]) - len(r)) for r in rows[1:]]
|
||||
self.matchers = []
|
||||
for row in self.rows:
|
||||
base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip()
|
||||
alts = [a.strip() for a in base.split(" / ") if a.strip()]
|
||||
pats = [re.compile(r"(?<![\w-])" + r"[\s-]+".join(map(re.escape, a.split())) + r"(?:s|es)?(?![\w-])", re.I) for a in alts]
|
||||
self.matchers.append((row, pats))
|
||||
|
||||
def column(self, lang):
|
||||
return self.header.index(lang) if lang in self.header else None
|
||||
|
||||
def matches(self, text):
|
||||
"""Glossary rows found in text, without terms only seen inside a longer match."""
|
||||
spans = []
|
||||
for row, pats in self.matchers:
|
||||
for p in pats:
|
||||
spans += [(m.start(), m.end(), row) for m in p.finditer(text)]
|
||||
keep = []
|
||||
for row in {id(r): r for _, _, r in spans}.values():
|
||||
mine = [(s, e) for s, e, r in spans if r is row]
|
||||
if any(not any(s2 <= s and e <= e2 and (e2 - s2) > (e - s) for s2, e2, r2 in spans if r2 is not row) for s, e in mine):
|
||||
keep.append(row)
|
||||
return keep
|
||||
|
||||
def hints(self, text, lang):
|
||||
col = self.column(lang)
|
||||
return [{"term": r[0], "translation": (r[col] if col is not None else "") or None, "description": r[1]} for r in self.matches(text)]
|
||||
|
||||
def language_notes(self, lang):
|
||||
text = self.md.read_text(encoding="utf-8-sig")
|
||||
m = re.search(r"^### [^\n]*\(" + re.escape(lang) + r"\)\s*$(.*?)(?=^##|\Z)", text, re.M | re.S)
|
||||
return m.group(0).strip() if m else None
|
||||
|
||||
|
||||
def _norm(s):
|
||||
s = unicodedata.normalize("NFD", s.casefold())
|
||||
return "".join(c for c in s if unicodedata.category(c) != "Mn")
|
||||
|
||||
|
||||
def glossary_ok(msgstr, translation):
|
||||
"""Loose check that a glossary rendering appears in msgstr, tolerating inflection."""
|
||||
target = _norm(msgstr)
|
||||
for alt in re.split(r"\s*/\s*", re.sub(r"\([^)]*\)|⚠", "", translation)):
|
||||
words = [w for w in re.findall(r"\w+", _norm(alt)) if len(w) > 2] or re.findall(r"\w+", _norm(alt))
|
||||
if not words:
|
||||
return True
|
||||
if all(w in target or (len(w) > 4 and w[:max(4, len(w) * 2 // 3)] in target) for w in words):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validation
|
||||
|
||||
C_FMT = re.compile(r"%(?:(\d+)\$)?[-+ #0']*(?:\d+|\*)?(?:\.(?:\d+|\*))?(?:hh|h|ll|l|L|q|j|z|Z|t)?[diouxXeEfFgGaAcspn]")
|
||||
BOOST_FMT = re.compile(r"%\d+%")
|
||||
TAG = re.compile(r"</?[A-Za-z][^<>]*>")
|
||||
SNAKE = re.compile(r"[a-z][a-z0-9]*(?:_[a-z0-9]+)+")
|
||||
IDENT = re.compile(r"\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b|\b[GM]\d{1,4}\b|\{[A-Za-z_][\w.\[\]]*\}|\[[a-z_]+\]")
|
||||
END = re.compile(r"[.:?!。:?!]$")
|
||||
|
||||
|
||||
def _placeholders(s, flags):
|
||||
s = s.replace("%%", "")
|
||||
boost = BOOST_FMT.findall(s)
|
||||
s2 = BOOST_FMT.sub("", s) if "boost-format" in flags or boost else s
|
||||
c = [m.group(0) for m in C_FMT.finditer(s2)] if "no-c-format" not in flags else []
|
||||
return boost, c
|
||||
|
||||
|
||||
def validate(e, msgstrs, nplurals, glossary=None, lang=None):
|
||||
"""Return (errors, warnings) for a proposed translation of entry e."""
|
||||
errors, warnings = [], []
|
||||
flags = set(e.flags)
|
||||
if e.msgid_plural is not None and len(msgstrs) != nplurals:
|
||||
errors.append(f"plural entry needs {nplurals} forms, got {len(msgstrs)}")
|
||||
for i, s in enumerate(msgstrs):
|
||||
src = e.msgid if (e.msgid_plural is None or (i == 0 and nplurals > 1)) else e.msgid_plural
|
||||
tag = f"[{i}] " if e.msgid_plural is not None else ""
|
||||
if not s:
|
||||
errors.append(f"{tag}empty translation")
|
||||
continue
|
||||
b1, c1 = _placeholders(src, flags)
|
||||
b2, c2 = _placeholders(s, flags)
|
||||
positional = any("$" in p for p in c1)
|
||||
c_bad = sorted(c1) != sorted(c2) if positional else c1 != c2
|
||||
bad = []
|
||||
if sorted(b1) != sorted(b2):
|
||||
bad.append(f"boost placeholders {b1} -> {b2}")
|
||||
if c_bad:
|
||||
bad.append(f"printf placeholders {c1} -> {c2}" + ("" if positional else " (order must match)"))
|
||||
if bad:
|
||||
strict = flags & {"c-format", "boost-format"} and not (e.msgid_plural is not None and i == 0 and nplurals > 1 and not c2 and not b2)
|
||||
(errors if strict else warnings).extend(tag + b for b in bad)
|
||||
if s.count("%%") != src.count("%%") and "c-format" in flags:
|
||||
errors.append(f"{tag}'%%' count differs")
|
||||
if src.count("\n") != s.count("\n"):
|
||||
errors.append(f"{tag}line breaks: {src.count(chr(10))} in source, {s.count(chr(10))} in translation")
|
||||
else:
|
||||
lead = lambda t: len(t) - len(t.lstrip("\n"))
|
||||
trail = lambda t: len(t) - len(t.rstrip("\n"))
|
||||
if lead(src) != lead(s) or trail(src) != trail(s):
|
||||
errors.append(f"{tag}leading/trailing line breaks differ")
|
||||
ls = lambda t: re.match(r"[ \t]*", t.strip("\n")).group(0)
|
||||
ts = lambda t: re.search(r"[ \t]*$", t.strip("\n")).group(0)
|
||||
if ls(src) != ls(s) or ts(src) != ts(s):
|
||||
errors.append(f"{tag}leading/trailing spaces differ")
|
||||
if sorted(TAG.findall(src)) != sorted(TAG.findall(s)):
|
||||
errors.append(f"{tag}HTML tags {TAG.findall(src)} -> {TAG.findall(s)}")
|
||||
if src.count("℃") != s.count("℃"):
|
||||
errors.append(f"{tag}'℃' count differs (keep U+2103, not °C)")
|
||||
missing = [t for t in set(IDENT.findall(src)) if t not in s]
|
||||
tokens = [t for t in missing if not SNAKE.fullmatch(t)]
|
||||
if tokens:
|
||||
errors.append(f"{tag}tokens must stay verbatim: {tokens}")
|
||||
if len(tokens) < len(missing): # snake_case is usually a setting key, sometimes a label
|
||||
warnings.append(f"{tag}snake_case kept? {[t for t in missing if SNAKE.fullmatch(t)]}")
|
||||
e1, e2 = END.search(src.rstrip()), END.search(s.rstrip())
|
||||
if bool(e1) != bool(e2) and lang != "th": # Thai has no sentence-final period
|
||||
warnings.append(f"{tag}ending punctuation: source {'ends with ' + repr(e1.group(0)) if e1 else 'has none'}, translation {'ends with ' + repr(e2.group(0)) if e2 else 'has none'}")
|
||||
if s == src and len(src.split()) >= 3:
|
||||
warnings.append(f"{tag}identical to the English")
|
||||
if glossary and lang:
|
||||
# Rows sharing a base term ("Seam", "Seam (UV)") are competing senses; one is enough.
|
||||
# A match on qualified rows only ("Feature (Design)") may be another sense entirely.
|
||||
for base, senses in group_senses(glossary.hints(src, lang)).items():
|
||||
if all(h["term"] != base for h in senses) or not strong_match(src, base):
|
||||
continue
|
||||
if not any(glossary_ok(s, h["translation"]) for h in senses):
|
||||
wanted = " or ".join(f"'{h['translation']}' ({h['term']})" for h in senses)
|
||||
warnings.append(f"{tag}glossary: expected {wanted}")
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def strong_match(src, base):
|
||||
"""Whether a term is surely meant as the glossary term, not as everyday English
|
||||
("each part of a frame"): a multi-word term, a short label, or capitalised mid-sentence."""
|
||||
if any(" " in alt for alt in base.split(" / ")) or len(src.split()) <= 5:
|
||||
return True
|
||||
for alt in base.split(" / "):
|
||||
for m in re.finditer(r"(?<![\w-])" + re.escape(alt) + r"(?:s|es)?(?![\w-])", src, re.I):
|
||||
if m.group(0)[0].isupper() and not re.search(r"(^|[.!?:]\s+|\n)$", src[:m.start()]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def base_term(term):
|
||||
return re.sub(r"\s*\([^)]*\)\s*$", "", term).strip()
|
||||
|
||||
|
||||
def group_senses(hints):
|
||||
groups = {}
|
||||
for h in hints:
|
||||
if h["translation"]:
|
||||
groups.setdefault(base_term(h["term"]), []).append(h)
|
||||
return groups
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
|
||||
def out_json(obj, path):
|
||||
text = json.dumps(obj, ensure_ascii=False, indent=1)
|
||||
if path:
|
||||
Path(path).write_text(text + "\n", encoding="utf-8")
|
||||
print(f"wrote {path}")
|
||||
else:
|
||||
sys.stdout.write(text + "\n")
|
||||
|
||||
|
||||
def cmd_status(a):
|
||||
print(f"{'lang':7} {'active':>7} {'empty':>6} {'plural':>6} {'fuzzy':>6} {'AI':>6} {'review':>6}")
|
||||
for lang in a.lang or all_langs():
|
||||
c = Catalog(catalog_path(lang))
|
||||
act = c.active()
|
||||
empty = sum(1 for e in act if needs_translation(e, c.nplurals) and e.msgid_plural is None)
|
||||
plural = sum(1 for e in act if needs_translation(e, c.nplurals) and e.msgid_plural is not None)
|
||||
print(f"{lang:7} {len(act):7} {empty:6} {plural:6} {sum(e.fuzzy for e in act):6} {sum(e.ai for e in act):6} {sum(bool(e.review) for e in act):6}")
|
||||
|
||||
|
||||
def similar_entries(c, e, pool, cutoff=0.72):
|
||||
"""The closest earlier translation of a similar English text: obsolete entries and translated
|
||||
active ones. A reworded or split string usually still has one, and reusing it keeps the wording."""
|
||||
import difflib
|
||||
sm = difflib.SequenceMatcher(autojunk=False)
|
||||
sm.set_seq2(e.msgid)
|
||||
best = []
|
||||
for other, msgstr, obsolete in pool:
|
||||
sm.set_seq1(other)
|
||||
if sm.real_quick_ratio() < cutoff or sm.quick_ratio() < cutoff:
|
||||
continue
|
||||
r = sm.ratio()
|
||||
if r >= cutoff and other != e.msgid:
|
||||
best.append((r, other, msgstr, obsolete))
|
||||
best.sort(key=lambda t: -t[0])
|
||||
return [{"msgid": o, "msgstr": s, "obsolete": ob, "similarity": round(r, 2)} for r, o, s, ob in best[:2]]
|
||||
|
||||
|
||||
def cmd_pending(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
g = Glossary(a.wiki)
|
||||
todo = [e for e in c.active() if needs_translation(e, c.nplurals, a.include_fuzzy)]
|
||||
batch = todo[a.offset:a.offset + a.limit] if a.limit else todo[a.offset:]
|
||||
pool = [(x.msgid, x.msgstrs()[0], x.obsolete) for x in c.entries
|
||||
if not x.is_header and x.msgstrs() and x.msgstrs()[0] and not x.fuzzy and 8 < len(x.msgid) < 600]
|
||||
items = []
|
||||
for e in batch:
|
||||
item = {"id": e.id(), "msgctxt": e.msgctxt, "msgid": e.msgid}
|
||||
if e.msgid_plural is not None:
|
||||
item["msgid_plural"] = e.msgid_plural
|
||||
item["msgstr_plural"] = (e.msgstrs() + [""] * c.nplurals)[:c.nplurals]
|
||||
else:
|
||||
item["msgstr"] = e.msgstrs()[0]
|
||||
if e.xcomments:
|
||||
item["note"] = " ".join(e.xcomments)
|
||||
flags = [f for f in e.flags if f != "fuzzy"]
|
||||
if flags:
|
||||
item["flags"] = flags
|
||||
if e.fuzzy:
|
||||
item["fuzzy"] = True
|
||||
if e.msgstrs()[0]:
|
||||
item["previous_translation"] = e.msgstrs()[0]
|
||||
if len(e.msgid) > 8:
|
||||
sim = similar_entries(c, e, pool)
|
||||
if sim:
|
||||
item["similar"] = sim
|
||||
hints = g.hints(e.msgid + "\n" + (e.msgid_plural or ""), a.lang)
|
||||
if hints:
|
||||
# Descriptions only where the sense must be chosen or no rendering exists yet.
|
||||
bases = [base_term(h["term"]) for h in hints]
|
||||
item["glossary"] = [{k: v for k, v in h.items() if k != "description" or not h["translation"] or bases.count(b) > 1 or b != h["term"]}
|
||||
for h, b in zip(hints, bases)]
|
||||
items.append({k: v for k, v in item.items() if v is not None})
|
||||
out_json({"lang": a.lang, "nplurals": c.nplurals, "plural_forms": re.search(r"Plural-Forms: ([^\n]*)", c.header.msgstrs()[0]).group(1) if c.header else None,
|
||||
"total_pending": len(todo), "offset": a.offset, "count": len(items), "entries": items}, a.out)
|
||||
|
||||
|
||||
def cmd_apply(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
data = json.loads(Path(a.input).read_text(encoding="utf-8"))
|
||||
items = data["entries"] if isinstance(data, dict) else data
|
||||
idx = c.index()
|
||||
by_id = {c.entries[i].id(): i for i in idx.values()}
|
||||
applied, skipped, rejected, warned = 0, 0, [], []
|
||||
for it in items:
|
||||
if "msgid" in it:
|
||||
key = (it.get("msgctxt"), it["msgid"])
|
||||
i = idx.get(key)
|
||||
label = (f"[{key[0]}] " if key[0] else "") + key[1][:70].replace("\n", "\\n")
|
||||
else:
|
||||
i = by_id.get(it.get("id"))
|
||||
label = f"id {it.get('id')}" if i is None else c.entries[i].msgid[:70].replace("\n", "\\n")
|
||||
if i is None:
|
||||
rejected.append((label, ["no such entry in the catalog (give its id, or msgid and msgctxt)"]))
|
||||
continue
|
||||
e = c.entries[i]
|
||||
if e.msgid_plural is not None and "msgstr_plural" not in it:
|
||||
rejected.append((label, [f"plural entry: give msgstr_plural with {c.nplurals} forms"]))
|
||||
continue
|
||||
strs = it["msgstr_plural"] if e.msgid_plural is not None else [it.get("msgstr", "")]
|
||||
if not strs or all(not s for s in strs):
|
||||
skipped += 1
|
||||
continue
|
||||
if not a.overwrite and not needs_translation(e, c.nplurals, include_fuzzy=True):
|
||||
rejected.append((label, ["already translated (pass --overwrite to replace)"]))
|
||||
continue
|
||||
errors, warnings = validate(e, strs, c.nplurals, g, a.lang)
|
||||
if errors:
|
||||
rejected.append((label, errors))
|
||||
continue
|
||||
if warnings:
|
||||
warned.append((label, warnings))
|
||||
c.changes[i] = e.rewrite(strs, ai=not a.human, review=it.get("review"))
|
||||
applied += 1
|
||||
if not a.dry_run and c.changes:
|
||||
c.save()
|
||||
for label, msgs in rejected:
|
||||
print(f"REJECTED {label}\n " + "\n ".join(msgs))
|
||||
for label, msgs in warned:
|
||||
print(f"WARNING {label}\n " + "\n ".join(msgs))
|
||||
print(f"{'would apply' if a.dry_run else 'applied'} {applied}, rejected {len(rejected)}, warnings {len(warned)}, blank skipped {skipped}")
|
||||
return 1 if rejected else 0
|
||||
|
||||
|
||||
def find_msgfmt():
|
||||
exe = REPO / "tools" / "msgfmt.exe"
|
||||
if os.name == "nt" and exe.exists():
|
||||
return str(exe)
|
||||
from shutil import which
|
||||
return which("msgfmt")
|
||||
|
||||
|
||||
def cmd_check(a):
|
||||
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
|
||||
msgfmt = find_msgfmt()
|
||||
status = 0
|
||||
for lang in a.lang or all_langs():
|
||||
c = Catalog(catalog_path(lang))
|
||||
errs = warns = 0
|
||||
for e in c.active():
|
||||
if a.scope == "ai" and not e.ai:
|
||||
continue
|
||||
strs = e.msgstrs()
|
||||
if all(s == "" for s in strs) or e.fuzzy:
|
||||
continue
|
||||
errors, warnings = validate(e, strs, c.nplurals, g, lang)
|
||||
if errors or (warnings and a.warnings):
|
||||
label = (f"[{e.msgctxt}] " if e.msgctxt else "") + e.msgid[:70].replace("\n", "\\n")
|
||||
print(f"{lang} line {e.first_line + 1}: {label}")
|
||||
for m in errors:
|
||||
print(f" ERROR {m}")
|
||||
for m in warnings if a.warnings else []:
|
||||
print(f" warn {m}")
|
||||
errs += bool(errors)
|
||||
warns += bool(warnings)
|
||||
if msgfmt:
|
||||
fd, mo = tempfile.mkstemp(suffix=".mo") # Windows msgfmt cannot write to "nul"
|
||||
os.close(fd)
|
||||
r = subprocess.run([msgfmt, "--check-format", "-o", mo, str(c.path)], capture_output=True, text=True)
|
||||
os.unlink(mo)
|
||||
if r.returncode:
|
||||
print(f"{lang}: msgfmt --check-format failed\n{r.stderr.strip()}")
|
||||
status = 1
|
||||
print(f"{lang}: {errs} entries with errors, {warns} with warnings (scope: {a.scope})")
|
||||
status |= bool(errs)
|
||||
if not msgfmt:
|
||||
print("note: msgfmt not found; skipped --check-format", file=sys.stderr)
|
||||
return status
|
||||
|
||||
|
||||
def cmd_lookup(a):
|
||||
c = Catalog(catalog_path(a.lang))
|
||||
rx = re.compile(a.pattern, re.I)
|
||||
n = 0
|
||||
for e in c.entries:
|
||||
strs = [s for s in e.msgstrs() if s]
|
||||
if e.is_header or not strs:
|
||||
continue
|
||||
texts = strs if a.msgstr else [e.msgid, e.msgid_plural or ""]
|
||||
if not any(rx.search(t) for t in texts):
|
||||
continue
|
||||
mark = "obsolete " if e.obsolete else f"{e.id()} " + ("AI " if e.ai else "")
|
||||
ctx = f"[{e.msgctxt}] " if e.msgctxt else ""
|
||||
print(f"{mark}{ctx}{json.dumps(e.msgid, ensure_ascii=False)}\n -> {json.dumps(strs[0] if len(strs) == 1 else strs, ensure_ascii=False)}")
|
||||
n += 1
|
||||
if n >= a.limit:
|
||||
print("... (limit reached)")
|
||||
break
|
||||
|
||||
|
||||
def cmd_glossary(a):
|
||||
g = Glossary(a.wiki)
|
||||
col = g.column(a.lang)
|
||||
if col is None:
|
||||
print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...")
|
||||
rx = re.compile(a.term, re.I) if a.term else None
|
||||
for r in g.rows:
|
||||
if rx and not rx.search(r[0]):
|
||||
continue
|
||||
val = r[col] if col is not None and r[col] else "—"
|
||||
print(f"{r[0]} => {val}\n {r[1]}")
|
||||
notes = g.language_notes(a.lang)
|
||||
if notes and not a.term:
|
||||
print("\n" + notes)
|
||||
|
||||
|
||||
STOP = set("""a an and are as at be been but by can could do does for from has have how if in into is it its
|
||||
may more most must no not of on once one only or other out over same set should so some such than that the
|
||||
their them then there these this those to too two up use used uses using very was when where whether which
|
||||
while will with without would you your yet all any each every per both also between before after first last
|
||||
off new now get gets let lets just already instead still even back down here make makes made via""".split())
|
||||
|
||||
|
||||
def _singular(key):
|
||||
return " ".join(re.sub(r"(?<=[a-z]{3})(es|s)$", "", w) for w in key.split())
|
||||
|
||||
|
||||
def kept_in_english(g):
|
||||
"""Names from the glossary page's "Kept in English" table."""
|
||||
text = g.md.read_text(encoding="utf-8-sig")
|
||||
m = re.search(r"^## Kept in English.*?$(.*?)(?=^## )", text, re.M | re.S)
|
||||
names = set()
|
||||
for row in re.findall(r"^\| (.*?) \|", m.group(1) if m else "", re.M):
|
||||
for piece in re.split(r"[,/:]|\bplus\b", row.replace("**", "").replace("`", "")):
|
||||
if piece.strip():
|
||||
names.add(_norm(piece.strip()))
|
||||
return names
|
||||
|
||||
|
||||
def cmd_candidates(a):
|
||||
g = Glossary(a.wiki)
|
||||
known = kept_in_english(g)
|
||||
for row, _ in g.matchers:
|
||||
known |= {_norm(x.strip()) for x in base_term(row[0]).split(" / ")}
|
||||
known |= {_singular(k) for k in known}
|
||||
known |= {w for k in known if " " in k for w in k.split()} # "tower" of "prime tower"
|
||||
pending = {}
|
||||
labels = set()
|
||||
for lang in all_langs():
|
||||
c = Catalog(catalog_path(lang))
|
||||
for e in c.active():
|
||||
labels.add(_norm(e.msgid))
|
||||
if needs_translation(e, c.nplurals):
|
||||
pending.setdefault(e.key(), set()).add(lang)
|
||||
counts = {} # phrase -> [strings, max langs, display, example, capitalised mid-sentence]
|
||||
for (ctx, msgid), langs in pending.items():
|
||||
seen = set()
|
||||
for chunk in re.split(r"[.,;:!?()\[\]\"\n—–]+|\s-\s", msgid):
|
||||
words = [re.sub(r"'s$", "", w) for w in re.findall(r"[A-Za-z][A-Za-z0-9-]*(?:'s)?", chunk)]
|
||||
# Capitals only mean a name inside an ordinary sentence, not in a Title Case label.
|
||||
sentence = len(words) >= 4 and sum(w[0].isupper() for w in words) * 2 < len(words)
|
||||
for n in (1, 2, 3):
|
||||
for i in range(len(words) - n + 1):
|
||||
gram = words[i:i + n]
|
||||
if gram[0].lower() in STOP or gram[-1].lower() in STOP or len("".join(gram)) < 4 \
|
||||
or any(w.lower() in STOP and w.lower() != "of" for w in gram):
|
||||
continue
|
||||
key = _norm(" ".join(gram))
|
||||
if key in seen or key in known or _singular(key) in known:
|
||||
continue
|
||||
seen.add(key)
|
||||
d = counts.setdefault(key, [0, 0, " ".join(gram), msgid, False])
|
||||
d[0] += 1
|
||||
d[1] = max(d[1], len(langs))
|
||||
d[4] |= sentence and i > 0 and all(w[0].isupper() for w in gram)
|
||||
# A term is a phrase, or a word capitalised mid-sentence or written as an acronym.
|
||||
keep = {k: v for k, v in counts.items()
|
||||
if v[0] >= a.min_count and (" " in k or v[4] or v[2].isupper())}
|
||||
# Drop fragments only ever seen inside a longer candidate ("IQEX parallel" in "IDEX IQEX parallel").
|
||||
keep = {k: v for k, v in keep.items()
|
||||
if not any(k != k2 and re.search(r"(^| )" + re.escape(k) + r"( |$)", k2) and v2[0] >= v[0] for k2, v2 in keep.items())}
|
||||
rows = sorted(keep.items(), key=lambda kv: (-kv[1][0], -len(kv[0].split())))
|
||||
print("strings langs phrase [label = also a msgid of its own] e.g.")
|
||||
for k, (cnt, langs, text, example, _) in rows[:a.limit]:
|
||||
ex = example.replace("\n", " ")
|
||||
print(f"{cnt:7} {langs:6} {text}{' [label]' if k in labels else ''} e.g. {ex[:70]}{'…' if len(ex) > 70 else ''}")
|
||||
|
||||
|
||||
def cmd_glossary_set(a):
|
||||
wiki = find_wiki(a.wiki)
|
||||
if not wiki:
|
||||
print("No OrcaSlicer_WIKI clone found (looked at --wiki, $ORCA_WIKI_DIR and next to this repository).\n"
|
||||
"The glossary must be updated with these terms before the translations that use them ship:\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)
|
||||
return 2
|
||||
tsv = wiki / GLOSSARY_TSV
|
||||
lock = tsv.with_suffix(".lock")
|
||||
for _ in range(100):
|
||||
try:
|
||||
fd = os.open(lock, os.O_CREAT | os.O_EXCL)
|
||||
break
|
||||
except FileExistsError:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
sys.exit(f"could not lock {lock}; remove it if no other run is active")
|
||||
try:
|
||||
data = tsv.read_bytes()
|
||||
bom = data.startswith(b"\xef\xbb\xbf")
|
||||
text = data.decode("utf-8-sig")
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
lines = text.splitlines()
|
||||
header = lines[0].split("\t")
|
||||
values = {}
|
||||
for s in a.set:
|
||||
lang, _, val = s.partition("=")
|
||||
if lang not in header:
|
||||
sys.exit(f"no glossary column '{lang}'")
|
||||
if "\t" in val or "\n" in val:
|
||||
sys.exit("values cannot contain tabs or line breaks")
|
||||
values[header.index(lang)] = val.strip()
|
||||
row_i = next((i for i, l in enumerate(lines[1:], 1) if l.split("\t")[0] == a.term), None)
|
||||
new = row_i is None
|
||||
if new:
|
||||
if not a.description:
|
||||
sys.exit("a new term needs --description (one sentence saying what it is)")
|
||||
row = [a.term, a.description] + [""] * (len(header) - 2)
|
||||
lines.append("")
|
||||
row_i = len(lines) - 1
|
||||
else:
|
||||
row = lines[row_i].split("\t")
|
||||
row += [""] * (len(header) - len(row))
|
||||
if a.description:
|
||||
row[1] = a.description
|
||||
conflicts = [f"{header[i]}: '{row[i]}' -> '{v}'" for i, v in values.items() if row[i].strip() not in MISSING and row[i] != v]
|
||||
if conflicts and not a.replace:
|
||||
sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts))
|
||||
for i, v in values.items():
|
||||
row[i] = v
|
||||
lines[row_i] = "\t".join(row)
|
||||
tsv.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(lock)
|
||||
print(f"glossary: {'added' if new else 'updated'} '{a.term}'")
|
||||
gen = wiki / "generate_glossary.py"
|
||||
if gen.exists():
|
||||
r = subprocess.run([sys.executable, str(gen), "--update"], cwd=wiki, capture_output=True, text=True)
|
||||
print((r.stdout + r.stderr).strip())
|
||||
return r.returncode
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("--wiki", help="path to an OrcaSlicer_WIKI clone")
|
||||
p.add_argument("--i18n", help="catalog root to use instead of localization/i18n")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
s = sub.add_parser("status")
|
||||
s.add_argument("--lang", action="append")
|
||||
s.set_defaults(func=cmd_status)
|
||||
|
||||
s = sub.add_parser("pending")
|
||||
s.add_argument("--lang", required=True)
|
||||
s.add_argument("--offset", type=int, default=0)
|
||||
s.add_argument("--limit", type=int, default=60)
|
||||
s.add_argument("--out")
|
||||
s.add_argument("--include-fuzzy", action=argparse.BooleanOptionalAction, default=True,
|
||||
help="list fuzzy entries too (default): users never see them")
|
||||
s.set_defaults(func=cmd_pending)
|
||||
|
||||
s = sub.add_parser("apply")
|
||||
s.add_argument("--lang", required=True)
|
||||
s.add_argument("--input", required=True)
|
||||
s.add_argument("--overwrite", action="store_true", help="also replace existing translations")
|
||||
s.add_argument("--human", action="store_true", help="do not add '# AI Translated' (a person wrote these)")
|
||||
s.add_argument("--dry-run", action="store_true")
|
||||
s.add_argument("--no-glossary", action="store_true")
|
||||
s.set_defaults(func=cmd_apply)
|
||||
|
||||
s = sub.add_parser("check")
|
||||
s.add_argument("--lang", action="append")
|
||||
s.add_argument("--scope", choices=["ai", "all"], default="ai")
|
||||
s.add_argument("--warnings", action="store_true", help="also print warnings")
|
||||
s.add_argument("--no-glossary", action="store_true")
|
||||
s.set_defaults(func=cmd_check)
|
||||
|
||||
s = sub.add_parser("lookup")
|
||||
s.add_argument("--lang", required=True)
|
||||
s.add_argument("pattern")
|
||||
s.add_argument("--msgstr", action="store_true", help="search the translations instead of the English")
|
||||
s.add_argument("--limit", type=int, default=40)
|
||||
s.set_defaults(func=cmd_lookup)
|
||||
|
||||
s = sub.add_parser("glossary")
|
||||
s.add_argument("--lang", required=True)
|
||||
s.add_argument("term", nargs="?")
|
||||
s.set_defaults(func=cmd_glossary)
|
||||
|
||||
s = sub.add_parser("candidates")
|
||||
s.add_argument("--min-count", type=int, default=3)
|
||||
s.add_argument("--limit", type=int, default=80)
|
||||
s.set_defaults(func=cmd_candidates)
|
||||
|
||||
s = sub.add_parser("glossary-set")
|
||||
s.add_argument("--term", required=True)
|
||||
s.add_argument("--description")
|
||||
s.add_argument("--set", action="append", default=[], metavar="LANG=VALUE")
|
||||
s.add_argument("--replace", action="store_true")
|
||||
s.set_defaults(func=cmd_glossary_set)
|
||||
|
||||
a = p.parse_args()
|
||||
if a.i18n:
|
||||
global I18N
|
||||
I18N = Path(a.i18n)
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
sys.exit(a.func(a) or 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
# both it and its tests live in scripts/, so a PR touching only those must still run
|
||||
# this workflow.
|
||||
- 'resources/printers/**'
|
||||
- 'resources/info/legacy_compatible_printers.json'
|
||||
- 'scripts/**'
|
||||
# orca_profile_tool.py reads the variant key sets from PrintConfig.cpp, and its
|
||||
# tests the obsolete keys, so a PR changing either must be checked against the profiles.
|
||||
@@ -59,6 +60,21 @@ jobs:
|
||||
python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
# The app migrates user filament presets made against 2.4.2 back to the printers their
|
||||
# parent listed then, from resources/info/legacy_compatible_printers.json. A PR that
|
||||
# renames a filament preset or narrows its compatible_printers changes what that map
|
||||
# must say, so it has to regenerate it.
|
||||
- name: Check the legacy compatible printers map
|
||||
id: legacy_compat
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -o pipefail
|
||||
base="${{ runner.temp }}/v2.4.2"
|
||||
git fetch --no-tags --depth=1 origin refs/tags/v2.4.2:refs/tags/v2.4.2
|
||||
mkdir -p "$base"
|
||||
git archive v2.4.2 resources/profiles | tar -x -C "$base"
|
||||
python3 ./scripts/orca_profile_tool.py legacy-compat --base "$base/resources/profiles" --check 2>&1 | tee ${{ runner.temp }}/legacy_compat.log
|
||||
|
||||
# download
|
||||
- name: Download
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -217,7 +233,7 @@ jobs:
|
||||
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
|
||||
|
||||
- name: Prepare comment artifact
|
||||
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.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: |
|
||||
{
|
||||
# Marker matched by check_profiles_comment.yml to delete prior comments.
|
||||
@@ -234,6 +250,17 @@ jobs:
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "${{ steps.legacy_compat.outcome }}" = "failure" ]; then
|
||||
echo "### Legacy Compatible Printers Map Out of Date"
|
||||
echo ""
|
||||
echo '```'
|
||||
head -c 30000 ${{ runner.temp }}/legacy_compat.log || echo "No output captured"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo 'Regenerate it: `git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2`, then `python3 scripts/orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles`.'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "${{ steps.validate_system.outcome }}" = "failure" ]; then
|
||||
echo "### System Profile Validation Failed"
|
||||
echo ""
|
||||
@@ -283,7 +310,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
- name: Fail if any check failed
|
||||
if: ${{ always() && (steps.profile_tool.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() && (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: |
|
||||
echo "One or more profile checks failed; see the step logs above."
|
||||
echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).'
|
||||
|
||||
@@ -67,6 +67,7 @@ ctest --test-dir ./tests/fff_print -C Release
|
||||
- Keep code concise and clear. Manually simplify AI generated bloated codes before review.
|
||||
- Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
|
||||
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped.
|
||||
- For new or changed UI strings, check they follow [Writing translatable strings in the source](#writing-translatable-strings-in-the-source): no concatenated fragments, plural forms for counts, contexts for ambiguous words.
|
||||
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) for that language.
|
||||
|
||||
## Localization & translations
|
||||
@@ -74,6 +75,19 @@ ctest --test-dir ./tests/fff_print -C Release
|
||||
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
|
||||
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md) for the human-facing version of these principles.
|
||||
|
||||
### Writing translatable strings in the source
|
||||
|
||||
Translators see one msgid at a time and cannot change the code around it, so each message must be complete in a single msgid. Use the wrapper that fits instead of assembling text from pieces:
|
||||
|
||||
- **One msgid per sentence, with placeholders.** Never build a message by concatenating translated fragments (`_L("A") + name + _L(" is B")`) or by splicing a translated word such as `_L("left")` into another sentence: the translator can't reorder or inflect the parts. Write `format(_L("%1% is too close to others"), name)`.
|
||||
- **A count needs a plural form.** Don't write `file(s)` or leave a verb unchanged after a number. Use `_L_PLURAL("%1% object", "%1% objects", n)`. libslic3r has no plural lookup, so word the message so it doesn't depend on a count, or build it in the GUI.
|
||||
- **An ambiguous word needs a context.** When a short string means different things in different places (verb or noun, a mode name, a CAD term), give each sense its own context with `_L_CONTEXT`/`_u8L_CONTEXT`/`L_CONTEXT`. Keep the most common sense without a context so its existing translations stay valid.
|
||||
- **Mark every user-visible string** (`_L`/`_u8L`, or `L` where it is translated later) and list new source files in `localization/i18n/list.txt`: files that aren't listed are never extracted.
|
||||
- Add a `// TRN` comment above a new string whose meaning or placeholders aren't obvious.
|
||||
- Never choose behavior by comparing translated text; compare ids or flags.
|
||||
|
||||
See `.claude/skills/orca-translations/references/source-audit.md` for the full checklist.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -88,18 +88,6 @@ struct NfpPConfig {
|
||||
*/
|
||||
bool explore_holes = false;
|
||||
|
||||
/**
|
||||
* @brief Keep the final pile on the bin.
|
||||
*
|
||||
* The final alignment centres the pile on the alignment target. A target
|
||||
* near an edge (a belt printer starts its parts at the leading end of the
|
||||
* belt) would push part of a pile that is larger than the room around that
|
||||
* point off the bed; with this set the pile stops at the edge instead, and a
|
||||
* pile that does not fit along an axis is centred on it. Off by default, so
|
||||
* the alignment of every other printer is unchanged.
|
||||
*/
|
||||
bool clamp_to_bin = false;
|
||||
|
||||
/**
|
||||
* @brief If true, use all CPUs available. Run on a single core otherwise.
|
||||
*/
|
||||
@@ -1125,22 +1113,22 @@ private:
|
||||
|
||||
auto d = cb - ci;
|
||||
|
||||
// Keep the pile on the bin (see Config::clamp_to_bin). The items' boxes carry
|
||||
// their inflation, which is the margin left at the edge.
|
||||
if (config_.clamp_to_bin) {
|
||||
auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
|
||||
if (hi - lo >= bin_hi - bin_lo)
|
||||
return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
|
||||
if (lo + shift < bin_lo)
|
||||
shift = bin_lo - lo;
|
||||
if (hi + shift > bin_hi)
|
||||
shift = bin_hi - hi;
|
||||
return shift;
|
||||
};
|
||||
setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d)));
|
||||
setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d)));
|
||||
cb = ci + d;
|
||||
}
|
||||
// Keep the pile on the bin. Centring it on a target near an edge (a belt
|
||||
// printer starts its parts at the leading end of the belt) would push a pile
|
||||
// larger than the room around that point off the bed, so it stops at the
|
||||
// edge instead; a pile that does not fit along an axis is centred on it.
|
||||
// The items' boxes carry their inflation, which is the margin left at the edge.
|
||||
auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
|
||||
if (hi - lo >= bin_hi - bin_lo)
|
||||
return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
|
||||
if (lo + shift < bin_lo)
|
||||
shift = bin_lo - lo;
|
||||
if (hi + shift > bin_hi)
|
||||
shift = bin_hi - hi;
|
||||
return shift;
|
||||
};
|
||||
setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d)));
|
||||
setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d)));
|
||||
|
||||
// BBS make sure the item won't clash with excluded regions
|
||||
// do we have wipe tower after arranging?
|
||||
@@ -1169,9 +1157,7 @@ private:
|
||||
return;
|
||||
}
|
||||
Item objs_convex_hull_item(objs_convex_hull);
|
||||
Vertex objs_convex_hull_ref = objs_convex_hull_item.referenceVertex();
|
||||
Vertex diff = objs_convex_hull_ref - sl::boundingBox(objs_convex_hull).center();
|
||||
Vertex ref_aligned = cb + diff; // reference point when pile center aligned with bed center
|
||||
Vertex ref_aligned = objs_convex_hull_item.referenceVertex() + d;
|
||||
bool ref_aligned_is_ok = std::any_of(nfps.begin(), nfps.end(), [&ref_aligned](auto& nfp) {return sl::isInside(ref_aligned, nfp); });
|
||||
if (!ref_aligned_is_ok) {
|
||||
// ref_aligned is not good, then find a nearest point on nfp boundary
|
||||
|
||||
+2153
-306
File diff suppressed because it is too large
Load Diff
+4188
-739
File diff suppressed because it is too large
Load Diff
+4244
-715
File diff suppressed because it is too large
Load Diff
+4175
-730
File diff suppressed because it is too large
Load Diff
+2153
-306
File diff suppressed because it is too large
Load Diff
+4136
-718
File diff suppressed because it is too large
Load Diff
+4215
-753
File diff suppressed because it is too large
Load Diff
+4146
-715
File diff suppressed because it is too large
Load Diff
+4206
-751
File diff suppressed because it is too large
Load Diff
+4119
-704
File diff suppressed because it is too large
Load Diff
+4117
-736
File diff suppressed because it is too large
Load Diff
+4084
-719
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,8 @@ src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoCut.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoCut.hpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp
|
||||
@@ -110,6 +112,7 @@ src/slic3r/GUI/Jobs/ArrangeJob.cpp
|
||||
src/slic3r/GUI/Jobs/OrientJob.cpp
|
||||
src/slic3r/GUI/Jobs/FillBedJob.cpp
|
||||
src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
|
||||
src/slic3r/GUI/Jobs/RotoptimizeJob.hpp
|
||||
src/slic3r/GUI/Jobs/BindJob.cpp
|
||||
src/slic3r/GUI/Jobs/PrintJob.cpp
|
||||
src/slic3r/GUI/Jobs/SendJob.cpp
|
||||
@@ -138,6 +141,7 @@ src/slic3r/GUI/DeviceErrorDialog.cpp
|
||||
src/slic3r/GUI/ExtraRenderers.cpp
|
||||
src/slic3r/GUI/EditGCodeDialog.cpp
|
||||
src/slic3r/GUI/Field.cpp
|
||||
src/slic3r/GUI/Field.hpp
|
||||
src/slic3r/GUI/GCodeViewer.cpp
|
||||
src/slic3r/GUI/GLCanvas3D.cpp
|
||||
src/slic3r/GUI/Calibration.cpp
|
||||
@@ -165,6 +169,7 @@ src/slic3r/GUI/PrintOptionsDialog.cpp
|
||||
src/slic3r/GUI/SafetyOptionsDialog.cpp
|
||||
src/slic3r/GUI/ParamsPanel.cpp
|
||||
src/slic3r/GUI/IMEXModesCtrl.cpp
|
||||
src/slic3r/GUI/IMEXFilamentPickerPopover.cpp
|
||||
src/slic3r/GUI/PartPlate.cpp
|
||||
src/slic3r/GUI/Plater.cpp
|
||||
src/slic3r/GUI/Preferences.cpp
|
||||
@@ -212,10 +217,14 @@ src/libslic3r/GCode/SeamPlacer.cpp
|
||||
src/libslic3r/ExtrusionEntity.cpp
|
||||
src/libslic3r/Flow.cpp
|
||||
src/libslic3r/Format/AMF.cpp
|
||||
src/libslic3r/Format/3mf.cpp
|
||||
src/libslic3r/Format/bbs_3mf.cpp
|
||||
src/libslic3r/miniz_extension.cpp
|
||||
src/libslic3r/IMEXHelpers.cpp
|
||||
src/libslic3r/Preset.cpp
|
||||
src/libslic3r/Print.cpp
|
||||
src/libslic3r/BeltPurge.cpp
|
||||
src/libslic3r/AppConfig.cpp
|
||||
src/libslic3r/PrintBase.cpp
|
||||
src/libslic3r/PrintConfig.cpp
|
||||
src/libslic3r/Zipper.cpp
|
||||
@@ -253,6 +262,7 @@ src/slic3r/Utils/MKS.cpp
|
||||
src/slic3r/Utils/Moonraker.cpp
|
||||
src/slic3r/Utils/OctoPrint.cpp
|
||||
src/slic3r/Utils/Repetier.cpp
|
||||
src/slic3r/Utils/ESP3D.cpp
|
||||
src/slic3r/GUI/SendMultiMachinePage.cpp
|
||||
src/slic3r/GUI/MultiMachinePage.cpp
|
||||
src/slic3r/GUI/MultiMachineManagerPage.cpp
|
||||
@@ -307,6 +317,8 @@ src/slic3r/GUI/AuxiliaryDataViewModel.cpp
|
||||
src/slic3r/GUI/AuxiliaryDialog.cpp
|
||||
src/slic3r/GUI/BBLStatusBarPrint.cpp
|
||||
src/slic3r/GUI/BBLStatusBarSend.cpp
|
||||
src/slic3r/GUI/BBLStatusBar.cpp
|
||||
src/slic3r/GUI/BeltPurgeTower.cpp
|
||||
src/slic3r/GUI/BaseTransparentDPIFrame.cpp
|
||||
src/slic3r/GUI/CloneDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeDialog.cpp
|
||||
@@ -347,11 +359,27 @@ src/slic3r/GUI/WebViewDialog.cpp
|
||||
src/slic3r/GUI/Widgets/AMSItem.cpp
|
||||
src/slic3r/GUI/Widgets/DialogButtons.cpp
|
||||
src/slic3r/Utils/NetworkAgentFactory.cpp
|
||||
src/slic3r/Utils/MoonrakerPrinterAgent.cpp
|
||||
src/slic3r/Utils/BBLNetworkPlugin.cpp
|
||||
src/slic3r/Utils/OrcaCloudServiceAgent.cpp
|
||||
src/slic3r/plugin/PluginAuditManager.cpp
|
||||
src/slic3r/plugin/PluginConfig.cpp
|
||||
src/slic3r/plugin/PluginResolver.cpp
|
||||
src/slic3r/plugin/CloudPluginService.cpp
|
||||
src/slic3r/plugin/PluginFsUtils.cpp
|
||||
src/slic3r/plugin/PluginHooks.cpp
|
||||
src/slic3r/plugin/PluginLoader.cpp
|
||||
src/slic3r/plugin/PluginManager.cpp
|
||||
src/slic3r/plugin/PythonPluginBridge.cpp
|
||||
src/libslic3r/CAD/SketchEngine.cpp
|
||||
src/libslic3r/CAD/GeometryEngine.cpp
|
||||
src/libslic3r/FilamentMixer.cpp
|
||||
src/libslic3r/Config.cpp
|
||||
src/libslic3r/Format/STEP.cpp
|
||||
src/libslic3r/Format/svg.cpp
|
||||
src/libslic3r/Extruder.cpp
|
||||
src/libslic3r/MeshBoolean.cpp
|
||||
src/libslic3r/utils.cpp
|
||||
src/slic3r/GUI/CAD/DesignOffer.hpp
|
||||
src/slic3r/GUI/CAD/DesignSketchTool.cpp
|
||||
src/slic3r/GUI/EncodedFilament.cpp
|
||||
|
||||
+4269
-738
File diff suppressed because it is too large
Load Diff
+4207
-752
File diff suppressed because it is too large
Load Diff
+4218
-748
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4274
-765
File diff suppressed because it is too large
Load Diff
+4277
-806
File diff suppressed because it is too large
Load Diff
+4134
-759
File diff suppressed because it is too large
Load Diff
+4157
-708
File diff suppressed because it is too large
Load Diff
+4386
-750
File diff suppressed because it is too large
Load Diff
+4119
-745
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
@@ -0,0 +1,795 @@
|
||||
{
|
||||
"presets": {
|
||||
"Anycubic Generic ABS": [
|
||||
"Anycubic Kobra 3 0.4 nozzle"
|
||||
],
|
||||
"Anycubic Generic TPU": [
|
||||
"Anycubic Kobra 3 0.4 nozzle"
|
||||
],
|
||||
"Bambu ABS @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu ABS @BBL X1C": [
|
||||
"Bambu Lab P1S 0.4 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu ASA @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PA6-CF @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PA6-CF @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PA6-CF @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PA6-CF @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PA6-CF @BBL X1C": [
|
||||
"Bambu Lab P1P 0.6 nozzle",
|
||||
"Bambu Lab P1P 0.8 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab P1S 0.8 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.8 nozzle"
|
||||
],
|
||||
"Bambu PA6-CF @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PAHT-CF @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PAHT-CF @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PAHT-CF @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PAHT-CF @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle",
|
||||
"Bambu Lab P1P 0.8 nozzle"
|
||||
],
|
||||
"Bambu PAHT-CF @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PAHT-CF @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab P1S 0.8 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.8 nozzle"
|
||||
],
|
||||
"Bambu PAHT-CF @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PC @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PETG @0.2 nozzle": [
|
||||
"Qidi Q1 Pro 0.2 nozzle"
|
||||
],
|
||||
"Bambu PETG Basic @BBL H2D 0.6 nozzle": [
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Basic @BBL H2DP 0.6 nozzle": [
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Basic @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Basic @BBL P2S 0.6 nozzle": [
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Basic @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG HF @BBL P2S 0.6 nozzle": [
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG HF @BBL X1C": [
|
||||
"Bambu Lab P1P 0.4 nozzle",
|
||||
"Bambu Lab P1S 0.4 nozzle"
|
||||
],
|
||||
"Bambu PETG HF @BBL X1C 0.8 nozzle": [
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PETG HF @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Translucent @BBL H2D 0.6 nozzle": [
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Translucent @BBL H2DP 0.6 nozzle": [
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Translucent @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Translucent @BBL P2S 0.6 nozzle": [
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG Translucent @BBL X1C 0.8 nozzle": [
|
||||
"Bambu Lab P1P 0.6 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PETG-CF @BBL H2D 0.6 nozzle": [
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG-CF @BBL H2DP 0.6 nozzle": [
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG-CF @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PETG-CF @BBL P2S 0.6 nozzle": [
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Basic @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Basic @BBL X1C": [
|
||||
"Bambu Lab P1S 0.4 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Basic @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Dynamic @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Dynamic @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Galaxy @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Galaxy @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Glow @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Glow @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Lite @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Lite @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Lite @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Lite @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Lite @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Marble @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle",
|
||||
"Bambu Lab P1P 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Marble @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Matte @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Matte @BBL X1C": [
|
||||
"Bambu Lab P1S 0.4 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Matte @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Metal @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Metal @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Silk+ @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Silk+ @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Silk+ @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Silk+ @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Silk+ @BBL X1C": [
|
||||
"Bambu Lab P1S 0.4 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Silk+ @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Sparkle @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Sparkle @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Tough+ @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Tough+ @BBL X1C": [
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Tough+ @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Translucent @BBL H2D 0.8 nozzle": [
|
||||
"Bambu Lab H2D 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Translucent @BBL H2DP 0.8 nozzle": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Translucent @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA Wood @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA Wood @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu PLA-CF @BBL H2D 0.6 nozzle": [
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA-CF @BBL H2DP 0.6 nozzle": [
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA-CF @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA-CF @BBL P2S 0.6 nozzle": [
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PLA-CF @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PPA-CF @BBL X1C": [
|
||||
"Bambu Lab P1P 0.6 nozzle",
|
||||
"Bambu Lab P1P 0.8 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab P1S 0.8 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.8 nozzle"
|
||||
],
|
||||
"Bambu PVA @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu PVA @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu PVA @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PVA @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu PVA @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA/PETG @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA/PETG @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA/PETG @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA/PETG @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support For PLA/PETG @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support for ABS @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support for ABS @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support for ABS @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support for ABS @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu Support for ABS @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU 90A @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU 90A @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU 95A HF @BBL H2D": [
|
||||
"Bambu Lab H2D 0.4 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU 95A HF @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.4 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU 95A HF @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU 95A HF @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU 95A HF @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU for AMS @BBL H2D": [
|
||||
"Bambu Lab H2D 0.6 nozzle",
|
||||
"Bambu Lab H2D 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU for AMS @BBL H2DP": [
|
||||
"Bambu Lab H2D Pro 0.6 nozzle",
|
||||
"Bambu Lab H2D Pro 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU for AMS @BBL H2S": [
|
||||
"Bambu Lab H2S 0.6 nozzle",
|
||||
"Bambu Lab H2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU for AMS @BBL P1P": [
|
||||
"Bambu Lab P1P 0.6 nozzle"
|
||||
],
|
||||
"Bambu TPU for AMS @BBL P2S": [
|
||||
"Bambu Lab P2S 0.6 nozzle",
|
||||
"Bambu Lab P2S 0.8 nozzle"
|
||||
],
|
||||
"Bambu TPU for AMS @BBL X1C": [
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 Carbon 0.6 nozzle"
|
||||
],
|
||||
"Bambu TPU for AMS @BBL X2D": [
|
||||
"Bambu Lab X2D 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic ABS": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic ASA": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic ASA-CF": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic PA": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic PA-CF": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic PC": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic PETG": [
|
||||
"BLOCKS RD50 V2 0.4 nozzle",
|
||||
"BLOCKS RD50 V2 0.6 nozzle",
|
||||
"BLOCKS RD50 V2 0.8 nozzle",
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic PLA": [
|
||||
"BLOCKS RD50 V2 0.4 nozzle",
|
||||
"BLOCKS RD50 V2 0.6 nozzle",
|
||||
"BLOCKS RD50 V2 0.8 nozzle",
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic PLA-CF": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic PVA": [
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Blocks Generic TPU": [
|
||||
"BLOCKS RD50 V2 0.4 nozzle",
|
||||
"BLOCKS RD50 V2 0.6 nozzle",
|
||||
"BLOCKS RD50 V2 0.8 nozzle",
|
||||
"BLOCKS RF50 0.4 nozzle",
|
||||
"BLOCKS RF50 0.6 nozzle",
|
||||
"BLOCKS RF50 0.8 nozzle"
|
||||
],
|
||||
"Creality Generic ABS": [
|
||||
"Creality CR-10 SE 0.2 nozzle",
|
||||
"Creality CR-10 SE 0.4 nozzle",
|
||||
"Creality CR-10 SE 0.6 nozzle",
|
||||
"Creality CR-10 SE 0.8 nozzle",
|
||||
"Creality CR-6 SE 0.2 nozzle",
|
||||
"Creality CR-6 SE 0.4 nozzle",
|
||||
"Creality CR-6 SE 0.6 nozzle",
|
||||
"Creality CR-6 SE 0.8 nozzle",
|
||||
"Creality CR-M4 0.4 nozzle",
|
||||
"Creality Ender-3 V2 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic ABS @Ender-3V3-all": [
|
||||
"Creality Ender-3 V3 Plus 0.4 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.6 nozzle"
|
||||
],
|
||||
"Creality Generic ABS @Hi-all": [
|
||||
"Creality Hi 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic ABS @K1-all": [
|
||||
"Creality K1 SE 0.4 nozzle",
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic ABS @K2-all": [
|
||||
"Creality K2 0.4 nozzle",
|
||||
"Creality K2 Plus 0.4 nozzle",
|
||||
"Creality K2 Plus 0.6 nozzle",
|
||||
"Creality K2 Plus 0.8 nozzle",
|
||||
"Creality K2 Pro 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic ASA @Ender-3V3-all": [
|
||||
"Creality Ender-3 V3 Plus 0.4 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.6 nozzle"
|
||||
],
|
||||
"Creality Generic ASA @K1-all": [
|
||||
"Creality K1 SE 0.4 nozzle",
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic ASA @K2-all": [
|
||||
"Creality K2 0.4 nozzle",
|
||||
"Creality K2 Plus 0.4 nozzle",
|
||||
"Creality K2 Pro 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PA-CF @Ender-3V3-all": [
|
||||
"Creality Ender-3 V3 Plus 0.4 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.6 nozzle"
|
||||
],
|
||||
"Creality Generic PA-CF @K1-all": [
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PA-CF @K2-all": [
|
||||
"Creality K2 Plus 0.4 nozzle",
|
||||
"Creality K2 Plus 0.6 nozzle",
|
||||
"Creality K2 Plus 0.8 nozzle"
|
||||
],
|
||||
"Creality Generic PC @K1-all": [
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PETG": [
|
||||
"Creality CR-6 SE 0.2 nozzle",
|
||||
"Creality CR-6 SE 0.4 nozzle",
|
||||
"Creality CR-6 SE 0.6 nozzle",
|
||||
"Creality CR-6 SE 0.8 nozzle",
|
||||
"Creality CR-M4 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PETG @Ender-3V3-all": [
|
||||
"Creality Ender-3 V3 0.4 nozzle",
|
||||
"Creality Ender-3 V3 0.6 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.4 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.6 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.2 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.4 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.6 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.8 nozzle"
|
||||
],
|
||||
"Creality Generic PETG @Hi-all": [
|
||||
"Creality Hi 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PETG @K1-all": [
|
||||
"Creality K1 SE 0.4 nozzle",
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PETG @K2-all": [
|
||||
"Creality K2 0.4 nozzle",
|
||||
"Creality K2 Plus 0.4 nozzle",
|
||||
"Creality K2 Plus 0.6 nozzle",
|
||||
"Creality K2 Plus 0.8 nozzle",
|
||||
"Creality K2 Pro 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PLA": [
|
||||
"Creality CR-10 SE 0.2 nozzle",
|
||||
"Creality CR-10 SE 0.4 nozzle",
|
||||
"Creality CR-10 SE 0.6 nozzle",
|
||||
"Creality CR-10 SE 0.8 nozzle",
|
||||
"Creality CR-6 SE 0.2 nozzle",
|
||||
"Creality CR-6 SE 0.4 nozzle",
|
||||
"Creality CR-6 SE 0.6 nozzle",
|
||||
"Creality CR-6 SE 0.8 nozzle",
|
||||
"Creality CR-M4 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PLA @Ender-3V3-all": [
|
||||
"Creality Ender-3 V3 0.4 nozzle",
|
||||
"Creality Ender-3 V3 0.6 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.4 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.6 nozzle"
|
||||
],
|
||||
"Creality Generic PLA @K1-all": [
|
||||
"Creality K1 SE 0.4 nozzle",
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PLA @K2-all": [
|
||||
"Creality K2 0.4 nozzle",
|
||||
"Creality K2 Plus 0.4 nozzle",
|
||||
"Creality K2 Plus 0.6 nozzle",
|
||||
"Creality K2 Plus 0.8 nozzle",
|
||||
"Creality K2 Pro 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PLA-CF @K1-all": [
|
||||
"Creality K1 SE 0.4 nozzle",
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic PLA-CF @K2-all": [
|
||||
"Creality K2 0.4 nozzle",
|
||||
"Creality K2 Plus 0.4 nozzle",
|
||||
"Creality K2 Pro 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic TPU": [
|
||||
"Creality CR-10 SE 0.2 nozzle",
|
||||
"Creality CR-10 SE 0.4 nozzle",
|
||||
"Creality CR-10 SE 0.6 nozzle",
|
||||
"Creality CR-10 SE 0.8 nozzle"
|
||||
],
|
||||
"Creality Generic TPU @Ender-3V3-all": [
|
||||
"Creality Ender-3 V3 0.4 nozzle",
|
||||
"Creality Ender-3 V3 0.6 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.4 nozzle",
|
||||
"Creality Ender-3 V3 Plus 0.6 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.2 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.4 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.6 nozzle",
|
||||
"Creality Ender-3 V3 SE 0.8 nozzle"
|
||||
],
|
||||
"Creality Generic TPU @K1-all": [
|
||||
"Creality K1 SE 0.4 nozzle",
|
||||
"Creality K1C 0.4 nozzle"
|
||||
],
|
||||
"Creality Generic TPU @K2-all": [
|
||||
"Creality K2 0.4 nozzle",
|
||||
"Creality K2 Plus 0.4 nozzle",
|
||||
"Creality K2 Pro 0.4 nozzle"
|
||||
],
|
||||
"Cubicon ABS @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon ABS-A100 @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon ABSk @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon PA-CF @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon PC @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon PETG @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon PLA @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon PLA+ @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Cubicon PLAi21 @base": [
|
||||
"Cubicon xCeler-Plus 0.4 nozzle"
|
||||
],
|
||||
"Flashforge Generic ABS @G3U": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle"
|
||||
],
|
||||
"Flashforge Generic ASA @G3U": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle"
|
||||
],
|
||||
"Flashforge Generic HIPS": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle"
|
||||
],
|
||||
"Flashforge Generic PETG @G3U": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle",
|
||||
"Flashforge Guider 3 Ultra 0.8 Nozzle"
|
||||
],
|
||||
"Flashforge Generic PETG-CF @G3U": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle",
|
||||
"Flashforge Guider 3 Ultra 0.8 Nozzle"
|
||||
],
|
||||
"Flashforge Generic PLA @G3U": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle",
|
||||
"Flashforge Guider 3 Ultra 0.8 Nozzle"
|
||||
],
|
||||
"Flashforge Generic PLA-CF @G3U": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle",
|
||||
"Flashforge Guider 3 Ultra 0.8 Nozzle"
|
||||
],
|
||||
"Flashforge HS PLA": [
|
||||
"Flashforge AD5X 0.4 nozzle",
|
||||
"Flashforge AD5X 0.6 nozzle",
|
||||
"Flashforge AD5X 0.8 nozzle"
|
||||
],
|
||||
"Flashforge PETG-CF": [
|
||||
"Flashforge AD5X 0.4 nozzle",
|
||||
"Flashforge AD5X 0.6 nozzle",
|
||||
"Flashforge AD5X 0.8 nozzle"
|
||||
],
|
||||
"Flashforge PLA-CF": [
|
||||
"Flashforge AD5X 0.4 nozzle",
|
||||
"Flashforge AD5X 0.6 nozzle",
|
||||
"Flashforge AD5X 0.8 nozzle"
|
||||
],
|
||||
"Flashforge TPU 95A": [
|
||||
"Flashforge AD5X 0.4 nozzle",
|
||||
"Flashforge AD5X 0.6 nozzle",
|
||||
"Flashforge AD5X 0.8 nozzle"
|
||||
],
|
||||
"FusRock Generic PAHT-CF": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle"
|
||||
],
|
||||
"FusRock Generic PET-CF": [
|
||||
"Flashforge Guider 3 Ultra 0.6 Nozzle"
|
||||
],
|
||||
"Generic PETG HF @BBL X1C": [
|
||||
"Bambu Lab P1S 0.4 nozzle",
|
||||
"Bambu Lab P1S 0.6 nozzle",
|
||||
"Bambu Lab X1 0.4 nozzle",
|
||||
"Bambu Lab X1 0.6 nozzle",
|
||||
"Bambu Lab X1 0.8 nozzle"
|
||||
],
|
||||
"Generic PLA @Hi-all": [
|
||||
"Creality Hi 0.4 nozzle",
|
||||
"Creality Hi 0.6 nozzle"
|
||||
],
|
||||
"HATCHBOX PETG @0.2 nozzle": [
|
||||
"Qidi Q1 Pro 0.2 nozzle"
|
||||
],
|
||||
"Prusa Generic PA": [
|
||||
"Prusa MINIIS 0.25 nozzle",
|
||||
"Prusa MINIIS 0.4 nozzle",
|
||||
"Prusa MINIIS 0.6 nozzle",
|
||||
"Prusa MINIIS 0.8 nozzle"
|
||||
],
|
||||
"Prusa Generic PA-CF": [
|
||||
"Prusa MINIIS 0.25 nozzle",
|
||||
"Prusa MINIIS 0.4 nozzle",
|
||||
"Prusa MINIIS 0.6 nozzle",
|
||||
"Prusa MINIIS 0.8 nozzle"
|
||||
],
|
||||
"Prusa Generic PLA-CF": [
|
||||
"Prusa MINIIS 0.25 nozzle",
|
||||
"Prusa MINIIS 0.4 nozzle",
|
||||
"Prusa MINIIS 0.6 nozzle",
|
||||
"Prusa MINIIS 0.8 nozzle"
|
||||
],
|
||||
"QIDI PETG Basic @0.2 nozzle": [
|
||||
"Qidi Q1 Pro 0.2 nozzle"
|
||||
],
|
||||
"QIDI PETG Rapido @0.2 nozzle": [
|
||||
"Qidi Q1 Pro 0.2 nozzle"
|
||||
],
|
||||
"QIDI PETG Translucent @0.2 nozzle": [
|
||||
"Qidi Q1 Pro 0.2 nozzle"
|
||||
],
|
||||
"Sovol Zero PETG HS Nozzle": [
|
||||
"Sovol Zero 0.4 nozzle"
|
||||
],
|
||||
"Sovol Zero PLA Basic HS Nozzle": [
|
||||
"Sovol Zero 0.4 nozzle"
|
||||
],
|
||||
"Sovol Zero PLA Silk HS Nozzle": [
|
||||
"Sovol Zero 0.4 nozzle"
|
||||
],
|
||||
"re3D Greengate rPETG": [
|
||||
"re3D GigabotX 2 1.75 nozzle",
|
||||
"re3D GigabotX 2 XLT 1.75 nozzle",
|
||||
"re3D TerabotX 2 1.75 nozzle"
|
||||
],
|
||||
"re3D PC": [
|
||||
"re3D Gigabot 4 0.8 nozzle",
|
||||
"re3D Gigabot 4 XLT 0.8 nozzle",
|
||||
"re3D Terabot 4 0.8 nozzle"
|
||||
],
|
||||
"re3D PETG": [
|
||||
"re3D Gigabot 4 0.8 nozzle",
|
||||
"re3D Gigabot 4 XLT 0.8 nozzle",
|
||||
"re3D Terabot 4 0.8 nozzle"
|
||||
],
|
||||
"re3D PLA": [
|
||||
"re3D Gigabot 4 0.8 nozzle",
|
||||
"re3D Gigabot 4 XLT 0.8 nozzle",
|
||||
"re3D Terabot 4 0.8 nozzle"
|
||||
],
|
||||
"re3D rPP": [
|
||||
"re3D GigabotX 2 1.75 nozzle",
|
||||
"re3D GigabotX 2 XLT 1.75 nozzle",
|
||||
"re3D TerabotX 2 1.75 nozzle"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div id="ctxMenu" class="ctx" hidden>
|
||||
<button class="ctx-item" data-action="open_folder" data-i18n="open_folder">Open folder in explorer</button>
|
||||
<button class="ctx-item" data-action="open_folder" data-i18n="open_folder">Show in folder</button>
|
||||
<button id = "delete_btn" class="ctx-item-delete" data-action="delete_bundle" data-i18n="delete_bundle" hidden>Delete bundle</button>
|
||||
<button id = "unsubscribe_btn" class="ctx-item-subscribed" data-action="unsubscribe_bundle" data-i18n="unsubscribe_bundle" hidden>Unsubscribe bundle</button>
|
||||
</div>
|
||||
|
||||
@@ -676,7 +676,7 @@ function labelParts(actions) {
|
||||
// Accessible label "Title from Pretty Source", disambiguated with the opaque action id when another
|
||||
// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically.
|
||||
function actionLabel(action, actions) {
|
||||
var label = action.title + " from " + prettySource(action.source || action.group || "");
|
||||
var label = T("sd_action_from", "%s from %s", action.title, prettySource(action.source || action.group || ""));
|
||||
if (actions && actions.length) {
|
||||
var cache = labelParts(actions);
|
||||
var mine = cache.sig[action.id];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script type="text/javascript" src="../include/i18n.js"></script>
|
||||
<style>
|
||||
* {
|
||||
user-select: none;
|
||||
@@ -525,7 +526,7 @@
|
||||
for (let j = 0; j < m_number_of_filaments; j++) {
|
||||
if (j == 0) {
|
||||
var tag = document.createElement("th")
|
||||
tag.innerHTML = "<div style='font-size:12px'>from/to</div>"
|
||||
tag.innerHTML = "<div style='font-size:12px'>" + T("from_to", "from/to") + "</div>"
|
||||
thead.appendChild(tag)
|
||||
}
|
||||
let th = document.createElement("th");
|
||||
|
||||
@@ -337,8 +337,10 @@ function UpdateSidebarVendors()
|
||||
{
|
||||
let SidebarHTML = "";
|
||||
$(`.OneVendorBlock`).each((i, el)=>{
|
||||
UpdateVendorCheckbox(el.getAttribute("vendor"));
|
||||
SidebarHTML +=`<div class="SidebarItem" onclick="scrollToVendor(this.textContent)">${el.getAttribute('vendor')}</div>`;
|
||||
let vendor = el.getAttribute('vendor');
|
||||
UpdateVendorCheckbox(vendor);
|
||||
let label = vendor == "Custom" ? T("custom_vendor", "Custom") : vendor;
|
||||
SidebarHTML +=`<div class="SidebarItem" vendor="${vendor}" onclick="scrollToVendor(this.getAttribute('vendor'))">${label}</div>`;
|
||||
});
|
||||
$('#SidebarVendors').html(SidebarHTML)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ function ShowRecentFileList( pList )
|
||||
//let index=sPath.lastIndexOf('\\')>0?sPath.lastIndexOf('\\'):sPath.lastIndexOf('\/');
|
||||
//let sShortName=sPath.substring(index+1,sPath.length);
|
||||
|
||||
let sBadge=sPublished? '<span class="FilePublishedBadge">PUB</span>':'';
|
||||
let sBadge=sPublished? '<span class="FilePublishedBadge">'+T("published_badge", "PUB")+'</span>':'';
|
||||
let sLogoBadge=sPublished? '<img class="FileLogoBadge" src="../../images/OrcaSlicer_gradient_circle.svg" alt="" />':'';
|
||||
|
||||
let TmpHtml='<div class="FileItem" fpath="'+sPath+'" >'+
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<div id="Model_Name_Area" class="Text_TitleBoard">
|
||||
<div id="Model_Name_Line" class="Text_TitleArea">
|
||||
<div class="Text_Title" id="ModelNameBar" ><div data-i18n="model_name">Model name</div><div>:</div><div title="xxxxxxxxxx" id="ModelName" style="margin-left: 8px;">xxxxxxxxxx</div></div>
|
||||
<div><span id="ModelAuthorType">Created by</span><span>:</span><span id="ModelAuthorName" style="margin-left: 8px;" class="Text_Value">xxxxxxx</span></div>
|
||||
<div><span id="ModelAuthorType" data-i18n="created_by">Created by</span><span>:</span><span id="ModelAuthorName" style="margin-left: 8px;" class="Text_Value">xxxxxxx</span></div>
|
||||
</div>
|
||||
<div id="ModelLicenceImg"><img src="img/by-nc-nd.png"></div>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,11 @@ docker run \
|
||||
--net=host \
|
||||
`# Some X installs will not have permissions to talk to sockets for shared memory` \
|
||||
--ipc host \
|
||||
`# Run as your workstations username to keep permissions the same` \
|
||||
-u "$USER" \
|
||||
`# Run as your workstations UID and GID to keep permissions the same.` \
|
||||
`# Numeric IDs work even when the image has no user with your name` \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-e HOME="/home/$USER" \
|
||||
-w "/home/$USER" \
|
||||
`# Bind mount your home directory into the container for loading/saving files` \
|
||||
-v "$HOME:/home/$USER" \
|
||||
`# Pass the X display number to the container` \
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
.DESCRIPTION
|
||||
The Windows counterpart of scripts/check_profile.sh, and kept deliberately close to it.
|
||||
|
||||
Runs the same five checks, in the same order, with the same validator flags, and with the
|
||||
Runs the same six checks, in the same order, with the same validator flags, and with the
|
||||
same semantics: every check runs even after an earlier one fails (the workflow's
|
||||
continue-on-error), then the script exits non-zero once at the end.
|
||||
|
||||
profile_tool scripts/orca_profile_tool.py check
|
||||
legacy_compat scripts/orca_profile_tool.py legacy-compat --check against
|
||||
the v2.4.2 profiles
|
||||
validate_system validator -p <profiles> -l <level>
|
||||
validate_slice validator -p <profiles> -s -l <level>
|
||||
validate_filament_subtypes validator -p <profiles> -l <level> -f
|
||||
@@ -20,8 +22,9 @@
|
||||
never sees a profile no <vendor>.json indexes, a preset name two files claim, or a file
|
||||
normalize and update-index would still rewrite.
|
||||
|
||||
Everything that has to be downloaded - the profile validator and the custom-preset fixture
|
||||
archives - lands under a per-user cache directory (%LOCALAPPDATA%\orca-profile-check) and
|
||||
Everything that has to be downloaded - the profile validator, the custom-preset fixture
|
||||
archives and the v2.4.2 profile tree - lands under a per-user cache directory
|
||||
(%LOCALAPPDATA%\orca-profile-check) and
|
||||
is reused on the next run. Being outside the checkout, that directory is shared by every
|
||||
worktree on the machine. It also holds one log per check plus a copy of the comment CI would
|
||||
post on the PR.
|
||||
@@ -48,7 +51,8 @@
|
||||
check it cannot narrow is validate_slice for a vendor that ships no printers; the summary
|
||||
reports that one as skipped, and naming it explicitly still runs it. profile_tool keeps its
|
||||
two cross-vendor checks (setting_id and filament_id) tree-wide, so a scoped run can still
|
||||
fail on another vendor's files.
|
||||
fail on another vendor's files, and legacy_compat is tree-wide too: the map it checks covers
|
||||
every vendor.
|
||||
|
||||
.PARAMETER Validator
|
||||
OrcaSlicer_profile_validator.exe to use; also $env:ORCA_PROFILE_VALIDATOR. Default: the local
|
||||
@@ -116,7 +120,7 @@ $HostArch = switch ($HostArch) {
|
||||
default { 'x86' }
|
||||
}
|
||||
|
||||
$AllChecks = @('profile_tool', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom')
|
||||
$AllChecks = @('profile_tool', 'legacy_compat', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom')
|
||||
|
||||
$script:LogWriter = $null
|
||||
$script:Python = ''
|
||||
@@ -439,6 +443,43 @@ function Expand-VendorPresets([string] $Zip, [string] $Tree, [string] $Prefix) {
|
||||
return $kept
|
||||
}
|
||||
|
||||
# The profile tree of the release resources\info\legacy_compatible_printers.json is generated
|
||||
# against. The tag is immutable, so the extracted tree is cached in the work dir and -Refresh
|
||||
# re-extracts it. A checkout without the tag (a shallow clone, a fork that never fetched tags)
|
||||
# gets it from upstream, as CI does. git archive writes a file rather than a pipe, which
|
||||
# PowerShell would re-encode, and tar.exe ships with Windows 10 and later.
|
||||
$LegacyBaseTag = 'v2.4.2'
|
||||
$LegacyBaseUrl = 'https://github.com/OrcaSlicer/OrcaSlicer.git'
|
||||
|
||||
function Get-LegacyBaseProfiles {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$base = Join-Path (Join-Path $WorkDir 'legacy-base') $LegacyBaseTag
|
||||
$profiles = Join-Path (Join-Path $base 'resources') 'profiles'
|
||||
if ($Refresh -or -not (Test-Path -LiteralPath $profiles -PathType Container)) {
|
||||
& git -C $RepoRoot rev-parse -q --verify "refs/tags/$LegacyBaseTag^{commit}" 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "fetching tag $LegacyBaseTag from $LegacyBaseUrl ..."
|
||||
& git -C $RepoRoot fetch --no-tags $LegacyBaseUrl "refs/tags/${LegacyBaseTag}:refs/tags/${LegacyBaseTag}"
|
||||
if ($LASTEXITCODE -ne 0) { throw "cannot fetch tag $LegacyBaseTag" }
|
||||
}
|
||||
# Extracted beside its final place and moved there whole, so an interrupted extraction is
|
||||
# never mistaken for the tree on the next run.
|
||||
$partial = "$base.partial"
|
||||
foreach ($dir in $base, $partial) {
|
||||
if (Test-Path -LiteralPath $dir) { Remove-Item -LiteralPath $dir -Recurse -Force }
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $partial | Out-Null
|
||||
$archive = Join-Path $partial 'profiles.tar'
|
||||
& git -C $RepoRoot archive -o $archive $LegacyBaseTag resources/profiles
|
||||
if ($LASTEXITCODE -ne 0) { throw "cannot archive resources/profiles of $LegacyBaseTag" }
|
||||
& tar -xf $archive -C $partial
|
||||
if ($LASTEXITCODE -ne 0) { throw "cannot extract $archive" }
|
||||
Remove-Item -LiteralPath $archive -Force
|
||||
Move-Item -LiteralPath $partial -Destination $base
|
||||
}
|
||||
return $profiles
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------- checks
|
||||
|
||||
$CheckBodies = @{
|
||||
@@ -447,6 +488,14 @@ $CheckBodies = @{
|
||||
Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check', '--profiles', $ProfilesDir) + $VendorPyArgs)
|
||||
}
|
||||
|
||||
# The map says which printers a user filament preset made against 2.4.2 can be migrated
|
||||
# back to, so a profile change that renames a filament preset or narrows its
|
||||
# compatible_printers has to regenerate it (the CI comment gives the command).
|
||||
legacy_compat = {
|
||||
$base = Get-LegacyBaseProfiles
|
||||
Invoke-Tool -Exe (Resolve-Python) -Arguments @((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'legacy-compat', '--profiles', $ProfilesDir, '--base', $base, '--check')
|
||||
}
|
||||
|
||||
validate_system = {
|
||||
Invoke-Tool -Exe $Validator -Arguments (@('-p', $ProfilesDir) + $VendorArgs + @('-l', "$LogLevel"))
|
||||
}
|
||||
@@ -581,6 +630,7 @@ $CheckBodies = @{
|
||||
# Heading CI puts above this check's log in the PR comment.
|
||||
$CommentHeadings = @{
|
||||
profile_tool = '### Profile Check Failed (orca_profile_tool.py)'
|
||||
legacy_compat = '### Legacy Compatible Printers Map Out of Date'
|
||||
validate_system = '### System Profile Validation Failed'
|
||||
validate_slice = '### Slice Validation Failed (custom g-code expansion)'
|
||||
validate_filament_subtypes = '### Filament Subtype Validation Failed'
|
||||
@@ -627,7 +677,7 @@ try {
|
||||
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
||||
Push-UserPresets
|
||||
|
||||
if ($Checks | Where-Object { $_ -ne 'profile_tool' }) { $Validator = Resolve-Validator }
|
||||
if ($Checks | Where-Object { $_ -ne 'profile_tool' -and $_ -ne 'legacy_compat' }) { $Validator = Resolve-Validator }
|
||||
|
||||
# An empty printer set is a failure to the sweep, so validate_slice is recorded as skipped
|
||||
# rather than run for a vendor that ships no printers (the filament-only OrcaFilamentLibrary);
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
#
|
||||
# Local twin of the "Check profiles" CI job (.github/workflows/check_profiles.yml).
|
||||
#
|
||||
# Runs the same five checks, in the same order, with the same validator flags, and with the
|
||||
# Runs the same six checks, in the same order, with the same validator flags, and with the
|
||||
# same semantics: every check runs even after an earlier one fails (the workflow's
|
||||
# continue-on-error), then the script exits non-zero once at the end.
|
||||
#
|
||||
# Everything that has to be downloaded - the profile validator and the custom-preset fixture
|
||||
# archives - lands under a per-user cache directory and is reused on the next run. Being outside
|
||||
# Everything that has to be downloaded - the profile validator, the custom-preset fixture
|
||||
# archives and the v2.4.2 profile tree - lands under a per-user cache directory and is reused on
|
||||
# the next run. Being outside
|
||||
# the checkout, that directory is shared by every worktree on the machine. It also holds one log
|
||||
# per check plus a copy of the comment CI would post on the PR.
|
||||
#
|
||||
@@ -58,7 +59,7 @@ LOG_LEVEL=2
|
||||
PREFER_DOWNLOAD=0
|
||||
REFRESH=0
|
||||
|
||||
ALL_CHECKS=(profile_tool validate_system validate_slice validate_filament_subtypes validate_custom)
|
||||
ALL_CHECKS=(profile_tool legacy_compat validate_system validate_slice validate_filament_subtypes validate_custom)
|
||||
CHECKS=()
|
||||
# "<check><TAB>pass|fail" per check that ran, plus "<check><TAB>skip<TAB>why" for one a vendor
|
||||
# scope left out; a string rather than an array because bash 3.2 (still the /bin/bash on macOS)
|
||||
@@ -73,6 +74,8 @@ Usage: scripts/check_profile.sh [OPTION]... [CHECK]...
|
||||
|
||||
Checks (default: all, in this order):
|
||||
profile_tool scripts/orca_profile_tool.py check
|
||||
legacy_compat scripts/orca_profile_tool.py legacy-compat --check against the
|
||||
v2.4.2 profiles
|
||||
validate_system validator -p <profiles> -l <level>
|
||||
validate_slice validator -p <profiles> -s -l <level>
|
||||
validate_filament_subtypes validator -p <profiles> -l <level> -f
|
||||
@@ -100,7 +103,8 @@ Note: --vendor narrows validate_custom too, by keeping only that vendor's preset
|
||||
fixture tree. The one check it cannot narrow is validate_slice for a vendor that ships no
|
||||
printers; the summary reports that one as skipped, and naming it explicitly still runs it.
|
||||
profile_tool keeps its two cross-vendor checks (setting_id and filament_id) tree-wide, so a
|
||||
scoped run can still fail on another vendor's files.
|
||||
scoped run can still fail on another vendor's files, and legacy_compat is tree-wide too: the map
|
||||
it checks covers every vendor.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -379,6 +383,44 @@ check_profile_tool() {
|
||||
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --profiles "${PROFILES_DIR}" --vendor "${VENDOR}"
|
||||
}
|
||||
|
||||
# The profile tree of the release resources/info/legacy_compatible_printers.json is generated
|
||||
# against. The tag is immutable, so the extracted tree is cached in the work dir and --refresh
|
||||
# re-extracts it. A checkout without the tag (a shallow clone, a fork that never fetched tags)
|
||||
# gets it from upstream, as CI does.
|
||||
LEGACY_BASE_TAG="v2.4.2"
|
||||
LEGACY_BASE_URL="https://github.com/OrcaSlicer/OrcaSlicer.git"
|
||||
|
||||
legacy_base_profiles() {
|
||||
local base="${WORK_DIR}/legacy-base/${LEGACY_BASE_TAG}"
|
||||
if [ "${REFRESH}" -eq 1 ] || [ ! -d "${base}/resources/profiles" ]; then
|
||||
if ! git -C "${REPO_ROOT}" rev-parse -q --verify "refs/tags/${LEGACY_BASE_TAG}^{commit}" >/dev/null; then
|
||||
msg "fetching tag ${LEGACY_BASE_TAG} from ${LEGACY_BASE_URL} ..."
|
||||
git -C "${REPO_ROOT}" fetch --no-tags "${LEGACY_BASE_URL}" \
|
||||
"refs/tags/${LEGACY_BASE_TAG}:refs/tags/${LEGACY_BASE_TAG}" || return 1
|
||||
fi
|
||||
# Extracted beside its final place and moved there whole, so an interrupted extraction is
|
||||
# never mistaken for the tree on the next run.
|
||||
local partial="${base}.partial"
|
||||
rm -rf "${base}" "${partial}"
|
||||
mkdir -p "${partial}" || return 1
|
||||
if ! git -C "${REPO_ROOT}" archive "${LEGACY_BASE_TAG}" resources/profiles | tar -x -C "${partial}"; then
|
||||
rm -rf "${partial}"
|
||||
return 1
|
||||
fi
|
||||
mv "${partial}" "${base}" || return 1
|
||||
fi
|
||||
printf '%s\n' "${base}/resources/profiles"
|
||||
}
|
||||
|
||||
# The map says which printers a user filament preset made against 2.4.2 can be migrated back
|
||||
# to, so a profile change that renames a filament preset or narrows its compatible_printers has
|
||||
# to regenerate it (the CI comment gives the command).
|
||||
check_legacy_compat() {
|
||||
local base
|
||||
base="$(legacy_base_profiles)" || return 1
|
||||
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" legacy-compat --profiles "${PROFILES_DIR}" --base "${base}" --check
|
||||
}
|
||||
|
||||
check_validate_system() {
|
||||
"${VALIDATOR}" -p "${PROFILES_DIR}" -v "${VENDOR}" -l "${LOG_LEVEL}"
|
||||
}
|
||||
@@ -563,6 +605,7 @@ EOF
|
||||
comment_heading() {
|
||||
case "$1" in
|
||||
profile_tool) echo "### Profile Check Failed (orca_profile_tool.py)" ;;
|
||||
legacy_compat) echo "### Legacy Compatible Printers Map Out of Date" ;;
|
||||
validate_system) echo "### System Profile Validation Failed" ;;
|
||||
validate_slice) echo "### Slice Validation Failed (custom g-code expansion)" ;;
|
||||
validate_filament_subtypes) echo "### Filament Subtype Validation Failed" ;;
|
||||
|
||||
@@ -11,6 +11,7 @@ commands:
|
||||
normalize rewrite profile files into their canonical shape
|
||||
trim delete profile files no <vendor>.json list references
|
||||
update-index regenerate the *_list sections of <vendor>.json
|
||||
legacy-compat write the printers user presets can be migrated back to
|
||||
|
||||
options shared by several commands:
|
||||
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
|
||||
@@ -24,6 +25,9 @@ After adding, renaming or deleting profile files, run:
|
||||
normalize -> update-index -> generate-id -> check
|
||||
normalize supplies missing types; update-index registers presets before id
|
||||
generation.
|
||||
After renaming filament presets or narrowing their compatible_printers, also
|
||||
rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2
|
||||
can be migrated back to their printers; CI runs it with --check.
|
||||
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
|
||||
the current index and can delete newly added, unindexed presets.
|
||||
|
||||
@@ -3072,6 +3076,139 @@ def fix_variant_arrays(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False, s
|
||||
return len(changed), errors
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# legacy-compat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
LEGACY_COMPAT_PATH = os.path.normpath(
|
||||
os.path.join(SCRIPTS_DIR, "..", "resources", "info", "legacy_compatible_printers.json"))
|
||||
|
||||
|
||||
def _renamed_from(data):
|
||||
"""A preset's renamed_from names. Profiles write one ';'-separated string, as the
|
||||
loader's unescape_strings_cstyle reads it; none of them quote or escape a name."""
|
||||
value = data.get("renamed_from") or []
|
||||
return [n for v in _as_list(value) for n in v.split(";") if n]
|
||||
|
||||
|
||||
def _flatten_presets(profiles_dir):
|
||||
"""Every bundle's composed filament and printer presets, for legacy-compat.
|
||||
|
||||
Returns (filaments, printers): filaments maps (vendor, name) to a dict with the
|
||||
resolved compatible_printers, filament_id, extruder variants, renamed_from and
|
||||
whether it is instantiated; printers maps each instantiated printer name to a
|
||||
dict with its renamed_from and extruder variants. instantiation is read from the
|
||||
preset's own file, as the loader requires every file to state it.
|
||||
"""
|
||||
def instantiated(data):
|
||||
return str(data.get("instantiation", "")).lower() == "true"
|
||||
|
||||
ofl = load_vendor_configs(profiles_dir, OFL)["filament"]
|
||||
filaments, printers = {}, {}
|
||||
for vendor in list_vendor_names(profiles_dir):
|
||||
configs = load_vendor_configs(profiles_dir, vendor)
|
||||
cache = {}
|
||||
for name, (_rel, data) in configs["filament"].items():
|
||||
config = compose_config(name, configs["filament"], ofl, cache, vendor == OFL)
|
||||
filaments[(vendor, name)] = {
|
||||
"compatible_printers": set(_as_list(config.get("compatible_printers", ([],))[0])),
|
||||
"filament_id": config.get("filament_id", (None,))[0],
|
||||
"variants": set(_as_list(config.get("filament_extruder_variant", ([],))[0])),
|
||||
"instantiation": instantiated(data),
|
||||
"renamed_from": _renamed_from(data),
|
||||
}
|
||||
cache = {}
|
||||
for name, (_rel, data) in configs["machine"].items():
|
||||
if instantiated(data):
|
||||
config = compose_config(name, configs["machine"], {}, cache)
|
||||
printers[name] = {
|
||||
"renamed_from": _renamed_from(data),
|
||||
"variants": set(_as_list(config.get("printer_extruder_variant", ([],))[0])),
|
||||
}
|
||||
return filaments, printers
|
||||
|
||||
|
||||
def legacy_compatible_printers(base_dir, profiles_dir=PROFILES_DIR):
|
||||
"""Printers each filament preset of the base release covered but the current
|
||||
preset it loads as no longer lists, kept only where a current preset with
|
||||
the same filament_id still covers that printer.
|
||||
|
||||
User presets store only their diff, so they inherit their parent's
|
||||
compatible_printers; when a profile change narrows that list, a user preset
|
||||
made against the base release disappears from printers it was used on. The
|
||||
app offers to write these printers into such user presets. Requiring a same-id
|
||||
successor leaves out printers a filament was deliberately taken off, and
|
||||
requiring a shared extruder variant keeps a printer from picking up another
|
||||
variant's values.
|
||||
|
||||
Keyed by the base release's preset name, which is what such a user preset's
|
||||
inherits still says. One current preset can stand in for several old ones
|
||||
through renamed_from, each with its own old list, so the key tells them
|
||||
apart. An old name resolves as the loader resolves an inherits: to the
|
||||
current selectable preset of that name, else to one listing it in
|
||||
renamed_from. A preset that is not instantiated never enters the loader's
|
||||
collection, so it does not stop the renamed_from lookup. Printer names are
|
||||
translated through the printers' renamed_from. Returns
|
||||
{old preset name: sorted printer names}.
|
||||
"""
|
||||
old_filaments, _ = _flatten_presets(base_dir)
|
||||
new_filaments, new_printers = _flatten_presets(profiles_dir)
|
||||
renamed_printer = {old: new for new, rec in new_printers.items() for old in rec["renamed_from"]}
|
||||
|
||||
by_name, by_old_name = {}, {}
|
||||
for (vendor, name), rec in new_filaments.items():
|
||||
if rec["instantiation"]:
|
||||
by_name.setdefault(name, []).append((vendor, rec))
|
||||
for old_name in rec["renamed_from"]:
|
||||
by_old_name.setdefault(old_name, []).append((vendor, rec))
|
||||
|
||||
covering = {} # filament_id -> printers some current instantiated preset covers
|
||||
for rec in new_filaments.values():
|
||||
if rec["instantiation"] and rec["filament_id"]:
|
||||
covering.setdefault(rec["filament_id"], set()).update(rec["compatible_printers"])
|
||||
|
||||
def shares_variant(filament, printer):
|
||||
variants = new_printers[printer]["variants"]
|
||||
return not filament["variants"] or not variants or bool(filament["variants"] & variants)
|
||||
|
||||
result = {}
|
||||
for (vendor, name), old in sorted(old_filaments.items()):
|
||||
# Only a selectable preset can be a user preset's parent.
|
||||
if not old["instantiation"]:
|
||||
continue
|
||||
candidates = by_name.get(name) or by_old_name.get(name, [])
|
||||
same_vendor = [r for v, r in candidates if v == vendor]
|
||||
rec = (same_vendor or [r for _, r in candidates] or [None])[0]
|
||||
# An empty list means every printer, so nothing can have been lost.
|
||||
if rec is None or not rec["compatible_printers"]:
|
||||
continue
|
||||
lost = {renamed_printer.get(p, p) for p in old["compatible_printers"]} - rec["compatible_printers"]
|
||||
lost &= covering.get(rec["filament_id"], set()) & set(new_printers)
|
||||
lost = {p for p in lost if shares_variant(rec, p)}
|
||||
if lost:
|
||||
result.setdefault(name, set()).update(lost)
|
||||
return {name: sorted(printers) for name, printers in result.items()}
|
||||
|
||||
|
||||
def run_legacy_compat(base_dir, profiles_dir, output, check):
|
||||
presets = legacy_compatible_printers(base_dir, profiles_dir)
|
||||
if check:
|
||||
try:
|
||||
with open(output, encoding="utf-8") as f:
|
||||
current = json.load(f).get("presets")
|
||||
except (OSError, ValueError, AttributeError):
|
||||
current = None
|
||||
if current != presets:
|
||||
print_error(f"{output} is out of date; rerun legacy-compat --base {base_dir}")
|
||||
return 1
|
||||
print_success(f"{output} is up to date")
|
||||
return 0
|
||||
with open(output, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(json.dumps({"presets": presets}, indent=4, sort_keys=True, ensure_ascii=False) + "\n")
|
||||
print_info(f"wrote {len(presets)} presets / {sum(map(len, presets.values()))} printers to {output}")
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -3093,11 +3230,17 @@ examples:
|
||||
preview resizing the variant arrays check rejects in that bundle
|
||||
orca_profile_tool.py check --strict --vendor BBL
|
||||
also hold every preset's inherited variant arrays to its own width
|
||||
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles
|
||||
rewrite resources/info/legacy_compatible_printers.json against 2.4.2's
|
||||
profiles (git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2)
|
||||
|
||||
after adding, renaming or deleting profile files, run in this order:
|
||||
normalize -> update-index -> generate-id -> check
|
||||
normalize supplies missing types; update-index registers presets before id
|
||||
generation.
|
||||
After renaming filament presets or narrowing their compatible_printers, also
|
||||
rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2
|
||||
can be migrated back to their printers; CI runs it with --check.
|
||||
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
|
||||
the current index and can delete newly added, unindexed presets.
|
||||
"""
|
||||
@@ -3227,6 +3370,23 @@ def build_parser():
|
||||
"Use trim only for deliberate unindexed-file cleanup, previewed with\n"
|
||||
"--dry-run; it can also delete newly authored presets.")
|
||||
|
||||
legacy_cmd = add(
|
||||
"legacy-compat", [profiles_opt],
|
||||
"write the printers user presets can be migrated back to",
|
||||
"Compare the tree with an older release's profiles and write, per filament\n"
|
||||
"preset of that release, the printers it covered then that the preset it now\n"
|
||||
"loads as no longer lists, where a preset with the same filament_id still\n"
|
||||
"covers them. The app offers to write these into user presets that inherit\n"
|
||||
"that preset by its old name and do not set compatible_printers, so a\n"
|
||||
"narrowed list does not hide them on printers they were made for.")
|
||||
legacy_cmd.add_argument("--base", required=True, metavar="DIR",
|
||||
help="the older release's resources/profiles directory")
|
||||
legacy_cmd.add_argument("--output", default=LEGACY_COMPAT_PATH, metavar="FILE",
|
||||
help="map to write (default: resources/info/"
|
||||
"legacy_compatible_printers.json)")
|
||||
legacy_cmd.add_argument("--check", action="store_true",
|
||||
help="fail if the map differs from what --base implies; write nothing")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -3260,6 +3420,9 @@ def main(argv=None):
|
||||
return run_generate_id(profiles_dir, vendors, args.filament_id, args.setting_id,
|
||||
args.dry_run)
|
||||
|
||||
if args.command == "legacy-compat":
|
||||
return run_legacy_compat(args.base, profiles_dir, args.output, args.check)
|
||||
|
||||
if args.command == "fix-variant":
|
||||
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
|
||||
strict=args.strict)
|
||||
|
||||
@@ -1258,6 +1258,118 @@ class TestVariantNames(TreeCase):
|
||||
self.assertIn('holds the legacy variant "Direct Drive Normal"', out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# legacy-compat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLegacyCompat(unittest.TestCase):
|
||||
"""A base release and the current tree, each with one vendor."""
|
||||
|
||||
def setUp(self):
|
||||
self.base, self.cur = Tree(), Tree()
|
||||
self.addCleanup(self.base.cleanup)
|
||||
self.addCleanup(self.cur.cleanup)
|
||||
|
||||
@staticmethod
|
||||
def printer(tree, name, **extra):
|
||||
tree.write("V", f"machine/{name}.json",
|
||||
{"type": "machine", "name": name, "instantiation": "true", **extra})
|
||||
tree.index("V", "machine", name, f"machine/{name}.json")
|
||||
|
||||
@staticmethod
|
||||
def filament(tree, name, printers, filament_id="OFpetg01", **extra):
|
||||
tree.write("V", f"filament/{name}.json",
|
||||
{"type": "filament", "name": name, "instantiation": "true",
|
||||
"filament_id": filament_id, "compatible_printers": printers, **extra})
|
||||
tree.index("V", "filament", name, f"filament/{name}.json")
|
||||
|
||||
def generate(self):
|
||||
return apt.legacy_compatible_printers(self.base.profiles, self.cur.profiles)
|
||||
|
||||
def test_renamed_and_narrowed_preset_keeps_printers_a_same_id_sibling_covers(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.6", "B 0.4"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PETG @A-all", ["A 0.4", "A 0.6", "B 0.4"])
|
||||
self.filament(self.cur, "PETG @V A-all", ["A 0.6"], renamed_from="PETG @A-all;PETG A-all")
|
||||
self.filament(self.cur, "PETG @A 0.4", ["A 0.4"])
|
||||
self.assertEqual(self.generate(), {"PETG @A-all": ["A 0.4"]})
|
||||
|
||||
def test_each_old_name_gets_back_only_its_own_list(self):
|
||||
# The current "PC @0.4" existed in the base and also replaces the split "PC".
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.8"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PC", ["A 0.4", "A 0.8"])
|
||||
self.filament(self.base, "PC @0.4", ["A 0.4"])
|
||||
self.filament(self.cur, "PC @0.4", ["A 0.4"], renamed_from="PC")
|
||||
self.filament(self.cur, "PC @0.8", ["A 0.8"])
|
||||
self.assertEqual(self.generate(), {"PC": ["A 0.8"]})
|
||||
|
||||
def test_old_name_now_a_base_preset_resolves_through_renamed_from(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("I 0.4", "Plus 0.4"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PLA @base", ["I 0.4", "Plus 0.4"])
|
||||
self.filament(self.cur, "PLA @base", [], instantiation="false")
|
||||
self.filament(self.cur, "PLA @I 0.4", ["I 0.4"], renamed_from="PLA @base")
|
||||
self.filament(self.cur, "PLA @Plus 0.4", ["Plus 0.4"])
|
||||
self.assertEqual(self.generate(), {"PLA @base": ["Plus 0.4"]})
|
||||
|
||||
def test_printer_without_a_successor_is_left_out(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.6"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"])
|
||||
self.filament(self.cur, "PETG @A", ["A 0.6"])
|
||||
self.filament(self.cur, "Other @A", ["A 0.4"], filament_id="OFother1")
|
||||
self.assertEqual(self.generate(), {})
|
||||
|
||||
def test_printer_names_follow_printer_renames(self):
|
||||
self.printer(self.base, "Old 0.4")
|
||||
self.printer(self.base, "A 0.6")
|
||||
self.printer(self.cur, "New 0.4", renamed_from="Old 0.4")
|
||||
self.printer(self.cur, "A 0.6")
|
||||
self.filament(self.base, "PETG @A", ["Old 0.4", "A 0.6"])
|
||||
self.filament(self.cur, "PETG @A", ["A 0.6"])
|
||||
self.filament(self.cur, "PETG @New", ["New 0.4"])
|
||||
self.assertEqual(self.generate(), {"PETG @A": ["New 0.4"]})
|
||||
|
||||
def test_printer_without_a_shared_extruder_variant_is_left_out(self):
|
||||
for tree in (self.base, self.cur):
|
||||
self.printer(tree, "Direct 0.4", printer_extruder_variant=["Direct Drive Standard"])
|
||||
self.printer(tree, "Bowden 0.4", printer_extruder_variant=["Bowden Standard"])
|
||||
self.filament(self.base, "PETG @all", ["Direct 0.4", "Bowden 0.4"])
|
||||
self.filament(self.cur, "PETG @all", ["Direct 0.4"],
|
||||
filament_extruder_variant=["Direct Drive Standard"])
|
||||
self.filament(self.cur, "PETG @Bowden", ["Bowden 0.4"])
|
||||
self.assertEqual(self.generate(), {})
|
||||
|
||||
def test_preset_now_listing_no_printer_is_skipped(self):
|
||||
for tree in (self.base, self.cur):
|
||||
self.printer(tree, "A 0.4")
|
||||
self.filament(self.base, "PETG @A", ["A 0.4"])
|
||||
self.filament(self.cur, "PETG @A", [])
|
||||
self.assertEqual(self.generate(), {})
|
||||
|
||||
def test_check_passes_on_a_current_map_and_fails_on_a_stale_one(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.6"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"])
|
||||
self.filament(self.cur, "PETG @A", ["A 0.6"])
|
||||
self.filament(self.cur, "PETG @A 0.4", ["A 0.4"])
|
||||
output = os.path.join(self.cur.dir, "map.json")
|
||||
argv = ["legacy-compat", "--base", self.base.profiles, "--profiles", self.cur.profiles,
|
||||
"--output", output]
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(apt.main(argv), 0)
|
||||
self.assertEqual(apt.main(argv + ["--check"]), 0)
|
||||
with open(output, "w", encoding="utf-8") as f:
|
||||
json.dump({"presets": {}}, f)
|
||||
self.assertEqual(apt.main(argv + ["--check"]), 1)
|
||||
|
||||
|
||||
class TestDispatch(TreeCase):
|
||||
def test_each_command_reaches_its_own_writer(self):
|
||||
self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"})
|
||||
|
||||
@@ -300,12 +300,10 @@ template<class PConf>
|
||||
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
|
||||
|
||||
if (params.is_belt) {
|
||||
// Pack from the end of the belt that prints first, and keep the pile on the
|
||||
// bed when it is larger than the room around that end.
|
||||
// Pack from the end of the belt that prints first.
|
||||
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
|
||||
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
|
||||
PConf::Alignment::BOTTOM_RIGHT;
|
||||
pcfg.clamp_to_bin = true;
|
||||
}
|
||||
else if (params.is_seq_print) {
|
||||
// Start placing the items from the center of the print bed
|
||||
|
||||
@@ -166,7 +166,7 @@ void Print::_plan_belt_purge()
|
||||
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
|
||||
|
||||
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
|
||||
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
|
||||
throw Slic3r::SlicingError(_u8L("The print is empty. The model is not printable with current print settings."));
|
||||
|
||||
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
|
||||
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
|
||||
|
||||
@@ -1807,7 +1807,8 @@ std::vector<std::pair<std::string, SketchPlane>> CadDocument::resolve_datum_plan
|
||||
|
||||
}
|
||||
|
||||
out.emplace_back(base_missing ? f.name + " (base plane missing)" : f.name, result);
|
||||
// TRN Datum plane entry in the reference plane list. %1% is the plane name.
|
||||
out.emplace_back(base_missing ? format(_u8L("%1% (base plane missing)"), f.name) : f.name, result);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -2090,8 +2091,8 @@ static int pick_body(int ref, int nb)
|
||||
{
|
||||
if (ref == -1) return nb - 1;
|
||||
if (ref < 0 || ref >= nb)
|
||||
throw std::runtime_error("a feature works on body " + std::to_string(ref + 1)
|
||||
+ ", which does not exist at that point in the history");
|
||||
throw std::runtime_error(format(_u8L("a feature works on body %1%, which does not exist at that point in the history"),
|
||||
ref + 1));
|
||||
return ref;
|
||||
}
|
||||
|
||||
@@ -2154,11 +2155,11 @@ static void settle_body_refs(CadFeature& f, const std::vector<CadBody>& v, bool
|
||||
CadFeature::BodyId& id = f.body_ref_ids[k++];
|
||||
if (kind != running) return;
|
||||
if (ref == kBodyGone)
|
||||
throw std::runtime_error("\"" + f.name + "\" works on a body made by a feature that was deleted or hidden");
|
||||
throw std::runtime_error(format(_u8L("\"%1%\" works on a body made by a feature that was deleted or hidden"), f.name));
|
||||
if (f.body_refs_pending && id.src >= 0) {
|
||||
const int i = find_body(v, id);
|
||||
if (i < 0)
|
||||
throw std::runtime_error("\"" + f.name + "\" works on a body that does not exist at that point in the history");
|
||||
throw std::runtime_error(format(_u8L("\"%1%\" works on a body that does not exist at that point in the history"), f.name));
|
||||
ref = i;
|
||||
}
|
||||
id = ref >= 0 ? body_id_of(v, ref) : CadFeature::BodyId{};
|
||||
@@ -2384,7 +2385,8 @@ bool CadDocument::move_feature(int index, int delta)
|
||||
check(f.sketch_ref); check(f.sweep_path_ref); check(f.pattern_curve_sketch); check(f.rib_sketch_ref);
|
||||
for (int& r : f.loft_profile_refs) check(r);
|
||||
if (!bad.empty()) {
|
||||
error = "\"" + f.name + "\" uses \"" + bad + "\", so it cannot come before it";
|
||||
// TRN Feature reorder error. %1% is the moved feature, %2% the feature it depends on.
|
||||
error = format(_u8L("\"%1%\" uses \"%2%\", so it cannot come before it"), f.name, bad);
|
||||
features.swap(snapshot);
|
||||
return false;
|
||||
}
|
||||
@@ -2463,7 +2465,8 @@ static std::string open_loop_message(const CadFeature& sketch,
|
||||
for (size_t i = 0; i < shown; ++i)
|
||||
msg += "\n " + format(_u8L("Open at (%.3f, %.3f)"), open[i].x(), open[i].y());
|
||||
if (open.size() > shown)
|
||||
msg += "\n " + format(_u8L("...and %1% more open end(s)"), open.size() - shown);
|
||||
// TRN Follows a list of open sketch ends. %1% is how many more were not listed.
|
||||
msg += "\n " + format(_u8L("Open ends not shown: %1%"), open.size() - shown);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
@@ -2475,11 +2478,11 @@ static gp_Ax1 revolve_axis_of(const CadFeature& f, const CadFeature& sk)
|
||||
if (f.revolve_axis_entity >= 0) {
|
||||
if (f.revolve_axis_entity >= int(sk.entities.size())
|
||||
|| sk.entities[f.revolve_axis_entity].type != SketchEntity::Type::Line)
|
||||
throw std::runtime_error("revolve: the axis line is no longer in the sketch — pick the axis again");
|
||||
throw std::runtime_error(_u8L("revolve: the axis line is no longer in the sketch — pick the axis again"));
|
||||
const SketchEntity& e = sk.entities[f.revolve_axis_entity];
|
||||
const Vec3d a = sk.plane.to_world(e.p0), b = sk.plane.to_world(e.p1);
|
||||
if ((b - a).norm() < 1e-9)
|
||||
throw std::runtime_error("revolve: the axis line has no length");
|
||||
throw std::runtime_error(_u8L("revolve: the axis line has no length"));
|
||||
const Vec3d d = (b - a).normalized();
|
||||
return gp_Ax1(gp_Pnt(a.x(), a.y(), a.z()), gp_Dir(d.x(), d.y(), d.z()));
|
||||
}
|
||||
@@ -3024,17 +3027,17 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
// ISO external thread depth is ~0.61*P, so allow up to 0.7*P (0.49 wrongly rejected
|
||||
// every real thread -> nothing rendered). Still bound it well under a full pitch.
|
||||
// R >= 0.5 (not >): M1, the smallest size in the standards table, is exactly 0.5.
|
||||
const char* why = R < 0.5 ? "thread: the diameter must be at least 1 mm"
|
||||
: P <= 0.1 ? "thread: the pitch must be more than 0.1 mm"
|
||||
: (D <= 1e-3 || D >= 0.7 * P) ? "thread: the depth must be between 0 and 0.7 x the pitch"
|
||||
: D >= 0.45 * R ? "thread: the depth is too large for this diameter"
|
||||
: H <= 0.5 * P ? "thread: the length must be more than half a pitch"
|
||||
: H / P >= 400.0 ? "thread: too many turns (length / pitch must stay under 400)"
|
||||
const char* why = R < 0.5 ? L("thread: the diameter must be at least 1 mm")
|
||||
: P <= 0.1 ? L("thread: the pitch must be more than 0.1 mm")
|
||||
: (D <= 1e-3 || D >= 0.7 * P) ? L("thread: the depth must be between 0 and 0.7 x the pitch")
|
||||
: D >= 0.45 * R ? L("thread: the depth is too large for this diameter")
|
||||
: H <= 0.5 * P ? L("thread: the length must be more than half a pitch")
|
||||
: H / P >= 400.0 ? L("thread: too many turns (length / pitch must stay under 400)")
|
||||
: nullptr;
|
||||
// Legacy recipes keep their old outcome (the body left as it was) so a project that
|
||||
// opened before still opens; every thread made now reports the reason instead.
|
||||
if (why != nullptr) {
|
||||
if (f.thread_major_nominal) throw std::runtime_error(why);
|
||||
if (f.thread_major_nominal) throw std::runtime_error(Slic3r::I18N::translate(why));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -3078,7 +3081,7 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
// Tapped hole: bore to the minor radius R - D, then cut the groove out to the
|
||||
// major radius R. The bore sits 10 um inside the minor radius so that, threading
|
||||
// an existing tap-drill hole, it does not coincide with that hole's wall.
|
||||
if (!have_body) throw std::runtime_error("internal thread needs a body");
|
||||
if (!have_body) throw std::runtime_error(_u8L("internal thread needs a body"));
|
||||
const double bore_r = R - D - 0.01;
|
||||
cut_from(result, BRepPrimAPI_MakeCylinder(ax2, bore_r, f.thread_height).Shape());
|
||||
TopoDS_Shape groove;
|
||||
@@ -3241,12 +3244,12 @@ void CadDocument::apply_boolean(std::vector<CadBody>& bodies, const CadFeature&
|
||||
const int tool = f.bool_tool_body == -1 ? -1 : pick_body(f.bool_tool_body, nb);
|
||||
// Same rule as Cut, Mirror and Transform: an operation that cannot run says why, rather
|
||||
// than leaving the bodies as they were and reporting success.
|
||||
if (tgt < 0) throw std::runtime_error("boolean: no target body");
|
||||
if (tool < 0) throw std::runtime_error("boolean: no tool body — pick a second body");
|
||||
if (tgt == tool) throw std::runtime_error("boolean: the target and the tool are the same body");
|
||||
if (tgt < 0) throw std::runtime_error(_u8L("boolean: no target body"));
|
||||
if (tool < 0) throw std::runtime_error(_u8L("boolean: no tool body — pick a second body"));
|
||||
if (tgt == tool) throw std::runtime_error(_u8L("boolean: the target and the tool are the same body"));
|
||||
const TopoDS_Shape A = bodies[tgt].shape; // target survives
|
||||
const TopoDS_Shape B = bodies[tool].shape; // tool, consumed unless kept
|
||||
if (A.IsNull() || B.IsNull()) throw std::runtime_error("boolean: a body has no geometry");
|
||||
if (A.IsNull() || B.IsNull()) throw std::runtime_error(_u8L("boolean: a body has no geometry"));
|
||||
|
||||
NCollection_List<TopoDS_Shape> args, tools;
|
||||
args.Append(A);
|
||||
@@ -3554,7 +3557,7 @@ void CadDocument::apply_thicken_surface(std::vector<CadBody>& bodies, const CadF
|
||||
if (props.Mass() < 0.0) solid.Reverse();
|
||||
}
|
||||
|
||||
bodies.push_back({solid, f.name.empty() ? std::string("ThickenSurface") : f.name});
|
||||
bodies.push_back({solid, f.name.empty() ? _u8L("Thicken Surface") : f.name});
|
||||
}
|
||||
|
||||
void CadDocument::apply_surface_offset(std::vector<CadBody>& bodies, const CadFeature& f) const
|
||||
@@ -3573,7 +3576,7 @@ void CadDocument::apply_surface_offset(std::vector<CadBody>& bodies, const CadFe
|
||||
TopoDS_Shape off_shape = mos.Shape();
|
||||
if (off_shape.IsNull()) throw std::runtime_error(_u8L("surface-offset: produced no geometry"));
|
||||
|
||||
bodies.push_back({off_shape, f.name.empty() ? std::string("SurfaceOffset") : f.name});
|
||||
bodies.push_back({off_shape, f.name.empty() ? _u8L("Surface Offset") : f.name});
|
||||
}
|
||||
|
||||
// Project `edges` of a shape onto `plane`, appending the resulting 2D entities to `out`.
|
||||
|
||||
@@ -325,10 +325,10 @@ std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid
|
||||
// handing the solid back unchanged, which recompute then reported as a success.
|
||||
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces)
|
||||
{
|
||||
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
|
||||
if (radius <= 0.001) throw std::runtime_error(_u8L("the fillet radius must be greater than 0"));
|
||||
|
||||
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
|
||||
if (edges.empty()) throw std::runtime_error("no edges to fillet in that group");
|
||||
if (edges.empty()) throw std::runtime_error(_u8L("no edges to fillet in that group"));
|
||||
|
||||
BRepFilletAPI_MakeFillet fillet(solid);
|
||||
for (const auto& edge : edges)
|
||||
@@ -344,10 +344,10 @@ TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radi
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, FaceGroup faces)
|
||||
{
|
||||
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
|
||||
if (distance <= 0.001) throw std::runtime_error(_u8L("the chamfer distance must be greater than 0"));
|
||||
|
||||
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
|
||||
if (edges.empty()) throw std::runtime_error("no edges to chamfer in that group");
|
||||
if (edges.empty()) throw std::runtime_error(_u8L("no edges to chamfer in that group"));
|
||||
|
||||
BRepFilletAPI_MakeChamfer chamfer(solid);
|
||||
for (const auto& edge : edges)
|
||||
@@ -370,7 +370,7 @@ TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double dis
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, const std::vector<int>& edge_ids)
|
||||
{
|
||||
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
|
||||
if (radius <= 0.001) throw std::runtime_error(_u8L("the fillet radius must be greater than 0"));
|
||||
if (edge_ids.empty()) throw std::runtime_error("apply_fillet: no edge picked");
|
||||
|
||||
BRepFilletAPI_MakeFillet mk(solid);
|
||||
@@ -387,7 +387,7 @@ TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radi
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, const std::vector<int>& edge_ids)
|
||||
{
|
||||
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
|
||||
if (distance <= 0.001) throw std::runtime_error(_u8L("the chamfer distance must be greater than 0"));
|
||||
if (edge_ids.empty()) throw std::runtime_error("apply_chamfer: no edge picked");
|
||||
|
||||
BRepFilletAPI_MakeChamfer mk(solid);
|
||||
|
||||
@@ -379,7 +379,7 @@ TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const gp_Ax1& a
|
||||
}
|
||||
}
|
||||
if (pos && neg)
|
||||
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
|
||||
throw std::runtime_error(_u8L("the profile crosses the revolve axis — it must lie on one side of it"));
|
||||
}
|
||||
gp_Ax1 axis = axis_in;
|
||||
double angle_rad = angle_deg * M_PI / 180.0;
|
||||
@@ -390,7 +390,7 @@ TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const gp_Ax1& a
|
||||
if (!rev.IsDone())
|
||||
throw std::runtime_error(_u8L("Failed to revolve"));
|
||||
if (!BRepCheck_Analyzer(rev.Shape()).IsValid())
|
||||
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
|
||||
throw std::runtime_error(_u8L("the profile crosses the revolve axis — it must lie on one side of it"));
|
||||
return rev.Shape();
|
||||
}
|
||||
|
||||
@@ -923,7 +923,7 @@ static TopoDS_Face checked_profile_face(const TopoDS_Face& f)
|
||||
{
|
||||
BRepCheck_Analyzer an(f);
|
||||
if (!an.IsValid())
|
||||
throw std::runtime_error("the profile crosses or folds back on itself, so it does not bound one region");
|
||||
throw std::runtime_error(_u8L("the profile crosses or folds back on itself, so it does not bound one region"));
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Exception.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "format.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Preset.hpp"
|
||||
@@ -613,7 +614,7 @@ bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src,
|
||||
void ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions_ctxt, bool append)
|
||||
{
|
||||
if (! this->set_deserialize_nothrow(opt_key_src, value_src, substitutions_ctxt, append))
|
||||
throw BadOptionValueException(format("Invalid value provided for parameter %1%: %2%", opt_key_src, value_src));
|
||||
throw BadOptionValueException(format(_u8L("Invalid value provided for parameter %1%: %2%"), opt_key_src, value_src));
|
||||
}
|
||||
|
||||
void ConfigBase::set_deserialize(std::initializer_list<SetDeserializeItem> items, ConfigSubstitutionContext& substitutions_ctxt)
|
||||
@@ -1173,7 +1174,7 @@ ConfigSubstitutions ConfigBase::load_from_ini(const std::string &file, ForwardCo
|
||||
boost::property_tree::read_ini(ifs, tree);
|
||||
return this->load(tree, compatibility_rule);
|
||||
} catch (const ConfigurationError &e) {
|
||||
throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file, e.what()));
|
||||
throw ConfigurationError(format(_u8L("Failed loading configuration file \"%1%\": %2%"), file, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,9 +1431,8 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
|
||||
}
|
||||
}
|
||||
if (!header_found) {
|
||||
std::string error_message = "Not a gcode file generated by ";
|
||||
error_message += SLIC3R_APP_FULL_NAME;
|
||||
error_message += ".";
|
||||
// TRN %1% is the application name.
|
||||
std::string error_message = format(_u8L("Not a gcode file generated by %1%."), SLIC3R_APP_FULL_NAME);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message;
|
||||
throw Slic3r::RuntimeError(error_message.c_str());
|
||||
}
|
||||
@@ -1458,7 +1458,8 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
|
||||
}
|
||||
if (!begin_found) {
|
||||
//BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << format("Configuration block closing tag \"; CONFIG_BLOCK_START\" not found when reading %1%", file);
|
||||
throw Slic3r::RuntimeError(format("Config tag \"; CONFIG_BLOCK_START\" not found"));
|
||||
// TRN %1% is a tag in the G-code file that must not be translated.
|
||||
throw Slic3r::RuntimeError(format(_u8L("Config tag \"%1%\" not found"), "; CONFIG_BLOCK_START"));
|
||||
}
|
||||
std::string key, value;
|
||||
while (std::getline(ifs, line)) {
|
||||
@@ -1483,7 +1484,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
|
||||
}
|
||||
if (!end_found) {
|
||||
//BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << format("Configuration block opening tag \"; CONFIG_BLOCK_END\" not found when reading %1%", file);
|
||||
throw Slic3r::RuntimeError(format("Config tag \"; CONFIG_BLOCK_END\" not found"));
|
||||
throw Slic3r::RuntimeError(format(_u8L("Config tag \"%1%\" not found"), "; CONFIG_BLOCK_END"));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1503,7 +1504,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
|
||||
|
||||
if (key_value_pairs < 80) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs);
|
||||
throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs));
|
||||
throw Slic3r::RuntimeError(format(_u8L("Suspiciously low number of configuration values extracted from %1%: %2%"), file, key_value_pairs));
|
||||
}
|
||||
|
||||
// Do legacy conversion on a completely loaded dictionary.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "Config.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -124,9 +125,9 @@ double Extruder::unretract()
|
||||
void Extruder::set_retracted(double retracted, double restart_extra)
|
||||
{
|
||||
if (retracted < - EPSILON)
|
||||
throw Slic3r::RuntimeError("Custom G-code reports negative z_retracted.");
|
||||
throw Slic3r::RuntimeError(_u8L("Custom G-code reports negative z_retracted."));
|
||||
if (restart_extra < - EPSILON)
|
||||
throw Slic3r::RuntimeError("Custom G-code reports negative z_restart_extra.");
|
||||
throw Slic3r::RuntimeError(_u8L("Custom G-code reports negative z_restart_extra."));
|
||||
|
||||
if (retracted > EPSILON) {
|
||||
m_retracted = retracted;
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "ColorDecomposeRecipe.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "FilamentMixerModel.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
|
||||
@@ -633,36 +635,36 @@ bool try_parse_mixed_components_strict(const std::string &str,
|
||||
{
|
||||
components.clear();
|
||||
if (str.empty()) {
|
||||
err = "empty component list";
|
||||
err = _u8L("empty component list");
|
||||
return false;
|
||||
}
|
||||
std::istringstream ss(str);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
if (token.empty()) {
|
||||
err = "empty component index";
|
||||
err = _u8L("empty component index");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const long val = std::stol(token);
|
||||
if (val < 1) {
|
||||
err = "component index must be >= 1 (got " + token + ")";
|
||||
err = format(_u8L("component index must be >= 1 (got %1%)"), token);
|
||||
return false;
|
||||
}
|
||||
components.push_back(static_cast<unsigned int>(val));
|
||||
} catch (...) {
|
||||
err = "invalid component index \"" + token + "\"";
|
||||
err = format(_u8L("invalid component index \"%1%\""), token);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (components.size() < 2) {
|
||||
err = "at least 2 components required (got " + std::to_string(components.size()) + ")";
|
||||
err = format(_u8L("at least 2 components required (got %1%)"), components.size());
|
||||
return false;
|
||||
}
|
||||
std::set<unsigned int> seen;
|
||||
for (unsigned int c : components) {
|
||||
if (!seen.insert(c).second) {
|
||||
err = "duplicate component index " + std::to_string(c);
|
||||
err = format(_u8L("duplicate component index %1%"), c);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -682,24 +684,24 @@ bool try_parse_mixed_ratios_strict(const std::string &str,
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
if (token.empty()) {
|
||||
err = "empty ratio value";
|
||||
err = _u8L("empty ratio value");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const double val = std::stod(token);
|
||||
if (!(val > 0.0)) {
|
||||
err = "ratio must be positive (got " + token + ")";
|
||||
err = format(_u8L("ratio must be positive (got %1%)"), token);
|
||||
return false;
|
||||
}
|
||||
ratios.push_back(val);
|
||||
} catch (...) {
|
||||
err = "invalid ratio \"" + token + "\"";
|
||||
err = format(_u8L("invalid ratio \"%1%\""), token);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (ratios.size() != n_components) {
|
||||
err = "expected " + std::to_string(n_components) + " ratio(s), got "
|
||||
+ std::to_string(ratios.size());
|
||||
// TRN %1% is the expected number of ratios, %2% the number found.
|
||||
err = format(_u8L("the ratio count must match the component count: expected %1%, got %2%"), n_components, ratios.size());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -713,11 +715,11 @@ bool validate_gradient_range_strict(const std::string &str, std::string &err)
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
float v0 = 0.f, v1 = 0.f;
|
||||
if (std::sscanf(str.c_str(), "%f,%f", &v0, &v1) != 2) {
|
||||
err = "expected two comma-separated floats, e.g. \"0.10,0.90\"";
|
||||
err = _u8L("expected two comma-separated floats, e.g. \"0.10,0.90\"");
|
||||
return false;
|
||||
}
|
||||
if (!(v0 > 0.f && v0 < 1.f && v1 > 0.f && v1 < 1.f)) {
|
||||
err = "start and end ratios must be in (0, 1)";
|
||||
err = _u8L("start and end ratios must be in (0, 1)");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -771,8 +773,7 @@ static void check_mixed_array_size_required(std::map<std::string, std::string> &
|
||||
{
|
||||
if (actual_size != expected_size) {
|
||||
append_error(errors, opt_key,
|
||||
"array size " + std::to_string(actual_size)
|
||||
+ " does not match filament slot count " + std::to_string(expected_size));
|
||||
format(_u8L("array size %1% does not match filament slot count %2%"), actual_size, expected_size));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,7 +790,7 @@ std::map<std::string, std::string> validate_mixed_filament_params(
|
||||
if (has_mixed_sub_params_specified(comp_strs, ratio_strs, gradient_flags)
|
||||
&& !has_any_mixed_filament(is_mixed)) {
|
||||
append_error(errors, "filament_is_mixed",
|
||||
"must be set when mixed filament parameters are specified");
|
||||
_u8L("must be set when mixed filament parameters are specified"));
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -822,34 +823,33 @@ std::map<std::string, std::string> validate_mixed_filament_params(
|
||||
if (!is_mixed[i])
|
||||
continue;
|
||||
|
||||
const std::string slot = "slot " + std::to_string(i + 1);
|
||||
const size_t slot = i + 1;
|
||||
const std::string comp_str = i < comp_strs.size() ? comp_strs[i] : "";
|
||||
|
||||
std::vector<unsigned int> components;
|
||||
std::string comp_err;
|
||||
if (!try_parse_mixed_components_strict(comp_str, components, comp_err)) {
|
||||
append_error(errors, "filament_mixed_components", slot + ": " + comp_err);
|
||||
// TRN Mixed filament validation error. %1% is the filament slot number, %2% the reason.
|
||||
append_error(errors, "filament_mixed_components", format(_u8L("slot %1%: %2%"), slot, comp_err));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (unsigned int c : components) {
|
||||
if (c > num_physical) {
|
||||
append_error(errors, "filament_mixed_components",
|
||||
slot + ": component " + std::to_string(c)
|
||||
+ " out of range (max physical filament index is "
|
||||
+ std::to_string(num_physical) + ")");
|
||||
format(_u8L("slot %1%: component %2% out of range (max physical filament index is %3%)"),
|
||||
slot, c, num_physical));
|
||||
break;
|
||||
}
|
||||
if (c == i + 1) {
|
||||
append_error(errors, "filament_mixed_components",
|
||||
slot + ": cannot reference itself as a component");
|
||||
format(_u8L("slot %1%: cannot reference itself as a component"), slot));
|
||||
break;
|
||||
}
|
||||
const size_t idx0 = static_cast<size_t>(c - 1);
|
||||
if (idx0 < is_mixed.size() && is_mixed[idx0]) {
|
||||
append_error(errors, "filament_mixed_components",
|
||||
slot + ": component " + std::to_string(c)
|
||||
+ " references a mixed filament slot");
|
||||
format(_u8L("slot %1%: component %2% references a mixed filament slot"), slot, c));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -857,27 +857,27 @@ std::map<std::string, std::string> validate_mixed_filament_params(
|
||||
std::string ratio_err;
|
||||
const std::string ratio_str = i < ratio_strs.size() ? ratio_strs[i] : "";
|
||||
if (!try_parse_mixed_ratios_strict(ratio_str, components.size(), ratio_err))
|
||||
append_error(errors, "filament_mixed_sublayer_ratios", slot + ": " + ratio_err);
|
||||
append_error(errors, "filament_mixed_sublayer_ratios", format(_u8L("slot %1%: %2%"), slot, ratio_err));
|
||||
|
||||
const bool gradient_on = i < gradient_flags.size() && gradient_flags[i];
|
||||
if (gradient_on) {
|
||||
if (components.size() != 2) {
|
||||
append_error(errors, "filament_mixed_gradient",
|
||||
slot + ": gradient requires exactly 2 components");
|
||||
format(_u8L("slot %1%: gradient requires exactly 2 components"), slot));
|
||||
}
|
||||
|
||||
if (gradient_specified) {
|
||||
std::string range_err;
|
||||
const std::string range_str = i < gradient_range_strs.size() ? gradient_range_strs[i] : "";
|
||||
if (!validate_gradient_range_strict(range_str, range_err))
|
||||
append_error(errors, "filament_mixed_gradient_range", slot + ": " + range_err);
|
||||
append_error(errors, "filament_mixed_gradient_range", format(_u8L("slot %1%: %2%"), slot, range_err));
|
||||
}
|
||||
|
||||
if (curve_specified) {
|
||||
const std::string curve_str = i < gradient_curve_strs.size() ? gradient_curve_strs[i] : "";
|
||||
if (!curve_str.empty() && parse_gradient_curve(curve_str).empty())
|
||||
append_error(errors, "filament_mixed_gradient_curve",
|
||||
slot + ": invalid curve (need at least 2 valid control points)");
|
||||
format(_u8L("slot %1%: invalid curve (need at least 2 valid control points)"), slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <string>
|
||||
#include <math.h>
|
||||
#include "PrintConfig.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
// Mark string for localization and translate.
|
||||
#define L(s) Slic3r::I18N::translate(s)
|
||||
@@ -17,10 +18,10 @@
|
||||
namespace Slic3r {
|
||||
|
||||
FlowErrorNegativeSpacing::FlowErrorNegativeSpacing() :
|
||||
FlowError("Flow::spacing() produced negative spacing. Did you set some extrusion width too small?") {}
|
||||
FlowError(L("Flow::spacing() produced negative spacing. Did you set some extrusion width too small?")) {}
|
||||
|
||||
FlowErrorNegativeFlow::FlowErrorNegativeFlow() :
|
||||
FlowError("Flow::mm3_per_mm() produced negative flow. Did you set some extrusion width too small?") {}
|
||||
FlowError(L("Flow::mm3_per_mm() produced negative flow. Did you set some extrusion width too small?")) {}
|
||||
|
||||
// This static method returns a sane extrusion width default.
|
||||
float Flow::auto_extrusion_width(FlowRole role, float nozzle_diameter)
|
||||
@@ -66,7 +67,8 @@ static inline FlowRole opt_key_to_flow_role(const std::string &opt_key)
|
||||
|
||||
static inline void throw_on_missing_variable(const std::string &opt_key, const char *dependent_opt_key)
|
||||
{
|
||||
throw FlowErrorMissingVariable((boost::format("Failed to calculate line width of %1%. Cannot get value of \u201c%2%\u201d.") % opt_key % dependent_opt_key).str());
|
||||
// TRN %1% and %2% are names of settings.
|
||||
throw FlowErrorMissingVariable((boost::format(L("Failed to calculate line width of %1%. Cannot get value of \"%2%\".")) % opt_key % dependent_opt_key).str());
|
||||
}
|
||||
|
||||
// Used to provide hints to the user on default extrusion width values, and to provide reasonable values to the PlaceholderParser.
|
||||
@@ -162,7 +164,7 @@ Flow Flow::with_spacing(float new_spacing) const
|
||||
assert(m_width >= m_height);
|
||||
out.m_width += new_spacing - m_spacing;
|
||||
if (out.m_width < out.m_height)
|
||||
throw Slic3r::InvalidArgument("Invalid spacing supplied to Flow::with_spacing(), check your layer height and extrusion width.");
|
||||
throw Slic3r::InvalidArgument(L("Invalid spacing supplied to Flow::with_spacing(), check your layer height and extrusion width."));
|
||||
}
|
||||
out.m_spacing = new_spacing;
|
||||
return out;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
|
||||
#include "STEP.hpp"
|
||||
|
||||
@@ -68,7 +69,7 @@ bool StepPreProcessor::preprocess(const char* path, std::string &output_path)
|
||||
{
|
||||
boost::nowide::ifstream infile(path);
|
||||
if (!infile.good()) {
|
||||
throw Slic3r::RuntimeError(std::string("Load step file failed.\nCannot open file for reading.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("Load step file failed.\nCannot open file for reading.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ bool StepPreProcessor::isUtf8File(const char* path)
|
||||
{
|
||||
boost::nowide::ifstream infile(path);
|
||||
if (!infile.good()) {
|
||||
throw Slic3r::RuntimeError(std::string("Load step file failed.\nCannot open file for reading.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("Load step file failed.\nCannot open file for reading.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -987,6 +987,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
MetadataList metadata;
|
||||
VolumeMetadataList volumes;
|
||||
bool auto_drop { true };
|
||||
};
|
||||
|
||||
struct CutObjectInfo
|
||||
@@ -1367,7 +1368,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
bool _handle_start_text_configuration(const char** attributes, unsigned int num_attributes);
|
||||
bool _handle_start_shape_configuration(const char **attributes, unsigned int num_attributes);
|
||||
|
||||
bool _create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, const bool auto_drop, unsigned int recur_counter);
|
||||
bool _create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter);
|
||||
|
||||
void _apply_transform(ModelInstance& instance, const Transform3d& transform);
|
||||
|
||||
@@ -2259,6 +2260,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (obj_metadata != m_objects_metadata.end()) {
|
||||
// config data has been found, this model was saved using slic3r pe
|
||||
|
||||
model_object->auto_drop = obj_metadata->second.auto_drop;
|
||||
|
||||
// apply object's name and config data
|
||||
for (const Metadata& metadata : obj_metadata->second.metadata) {
|
||||
if (metadata.key == "name")
|
||||
@@ -4117,9 +4120,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR);
|
||||
Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR));
|
||||
int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR);
|
||||
int auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
|
||||
|
||||
return !m_load_model || _create_object_instance(path, object_id, transform, printable, auto_drop, 1);
|
||||
return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1);
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Importer::_handle_end_item()
|
||||
@@ -4347,7 +4348,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Importer::_create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, const bool auto_drop, unsigned int recur_counter)
|
||||
bool _BBS_3MF_Importer::_create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter)
|
||||
{
|
||||
static const unsigned int MAX_RECURSIONS = 10;
|
||||
|
||||
@@ -4384,7 +4385,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return false;
|
||||
}
|
||||
instance->printable = printable;
|
||||
instance->auto_drop = auto_drop;
|
||||
|
||||
m_instances.emplace_back(instance, transform);
|
||||
|
||||
@@ -4417,7 +4417,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return false;
|
||||
}
|
||||
instance->printable = printable;
|
||||
instance->auto_drop = auto_drop;
|
||||
|
||||
m_instances.emplace_back(instance, transform);
|
||||
}
|
||||
@@ -4488,7 +4487,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// Added because of github #3435, currently not used by PrusaSlicer
|
||||
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
|
||||
|
||||
m_objects_metadata.insert({ object_id, ObjectMetadata() });
|
||||
ObjectMetadata metadata;
|
||||
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
|
||||
m_objects_metadata.insert({ object_id, std::move(metadata) });
|
||||
m_curr_config.object_id = object_id;
|
||||
return true;
|
||||
}
|
||||
@@ -6164,14 +6165,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
unsigned int id;
|
||||
Transform3d transform;
|
||||
bool printable;
|
||||
bool auto_drop;
|
||||
|
||||
BuildItem(std::string const & path, unsigned int id, const Transform3d& transform, const bool printable, const bool auto_drop)
|
||||
BuildItem(std::string const & path, unsigned int id, const Transform3d& transform, const bool printable)
|
||||
: path(path)
|
||||
, id(id)
|
||||
, transform(transform)
|
||||
, printable(printable)
|
||||
, auto_drop(auto_drop)
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -6262,7 +6261,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::vector<std::string> const &targets = {},
|
||||
std::vector<std::string> const &types = {},
|
||||
PackingTemporaryData data = PackingTemporaryData(),
|
||||
int export_plate_idx = -1) const;
|
||||
int export_plate_idx = -1,
|
||||
bool plate_thumbnail_written = false,
|
||||
bool plate_small_thumbnail_written = false) const;
|
||||
bool _add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, ObjectToObjectDataMap& objects_data, Export3mfProgressFn proFn = nullptr, BBLProject* project = nullptr) const;
|
||||
bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, ObjectData const &object_data) const;
|
||||
void _add_object_components_to_stream(std::stringstream &stream, ObjectData const &object_data) const;
|
||||
@@ -6472,6 +6473,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(",top&&pick thumbnails, count %1%")%top_thumbnail_data.size();
|
||||
|
||||
//BBS: add thumbnail for each plate
|
||||
//ORCA: the relationships file points at plate 1's thumbnail for a whole-project export,
|
||||
// otherwise at the exported plate's. Track whether that thumbnail was written.
|
||||
const size_t rels_plate_idx = (export_plate_idx < 0) ? 0 : static_cast<size_t>(export_plate_idx);
|
||||
bool rels_thumbnail_written = false;
|
||||
bool rels_small_thumbnail_written = false;
|
||||
if (!m_skip_static) {
|
||||
std::vector<bool> thumbnail_status(plate_data_list.size(), false);
|
||||
std::vector<bool> no_light_thumbnail_status(plate_data_list.size(), false);
|
||||
@@ -6520,6 +6526,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(",add thumbnail %1%'s data into 3mf")%(index+1);
|
||||
thumbnail_status[index] = true;
|
||||
if (static_cast<size_t>(index) == rels_plate_idx) {
|
||||
//ORCA: this call also writes the _small companion
|
||||
rels_thumbnail_written = true;
|
||||
rels_small_thumbnail_written = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6566,6 +6577,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":" << __LINE__ << boost::format(", add thumbnail %1% from file %2% failed\n") % (i+1) %plate_data->thumbnail_file;
|
||||
return false;
|
||||
}
|
||||
//ORCA: copied from file, so there is no _small companion for this plate
|
||||
if (static_cast<size_t>(i) == rels_plate_idx)
|
||||
rels_thumbnail_written = true;
|
||||
}
|
||||
|
||||
if (!no_light_thumbnail_status[i] && !plate_data->no_light_thumbnail_file.empty() && (boost::filesystem::exists(plate_data->no_light_thumbnail_file))){
|
||||
@@ -6860,7 +6874,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// Adds relationships file ("_rels/.rels").
|
||||
// The content of this file is the same for each OrcaSlicer 3mf.
|
||||
// The relationshis file contains a reference to the geometry file "3D/3dmodel.model", the name was chosen to be compatible with CURA.
|
||||
if (!_add_relationships_file_to_archive(archive, {}, {}, {}, temp_data, export_plate_idx)) {
|
||||
if (!_add_relationships_file_to_archive(archive, {}, {}, {}, temp_data, export_plate_idx, rels_thumbnail_written, rels_small_thumbnail_written)) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", _add_relationships_file_to_archive failed\n");
|
||||
return false;
|
||||
}
|
||||
@@ -7035,7 +7049,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_relationships_file_to_archive(
|
||||
mz_zip_archive &archive, std::string const &from, std::vector<std::string> const &targets, std::vector<std::string> const &types, PackingTemporaryData data, int export_plate_idx) const
|
||||
mz_zip_archive &archive, std::string const &from, std::vector<std::string> const &targets, std::vector<std::string> const &types, PackingTemporaryData data, int export_plate_idx,
|
||||
bool plate_thumbnail_written, bool plate_small_thumbnail_written) const
|
||||
{
|
||||
std::stringstream stream;
|
||||
stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||||
@@ -7046,7 +7061,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (export_plate_idx < 0) {
|
||||
//use cover image if have
|
||||
if (data._3mf_thumbnail.empty()) {
|
||||
stream << " <Relationship Target=\"/Metadata/plate_1.png"
|
||||
// ORCA: only reference the plate thumbnail if it was written
|
||||
if (plate_thumbnail_written)
|
||||
stream << " <Relationship Target=\"/Metadata/plate_1.png"
|
||||
<< "\" Id=\"rel-2\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail\"/>\n";
|
||||
} else {
|
||||
stream << " <Relationship Target=\"/" << xml_escape(data._3mf_thumbnail)
|
||||
@@ -7054,22 +7071,24 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
|
||||
if (data._3mf_printer_thumbnail_middle.empty()) {
|
||||
stream << " <Relationship Target=\"/Metadata/plate_1.png"
|
||||
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
|
||||
if (plate_thumbnail_written)
|
||||
stream << " <Relationship Target=\"/Metadata/plate_1.png"
|
||||
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
|
||||
} else {
|
||||
stream << " <Relationship Target=\"/" << xml_escape(data._3mf_printer_thumbnail_middle)
|
||||
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
|
||||
}
|
||||
|
||||
if (data._3mf_printer_thumbnail_small.empty()) {
|
||||
stream << "<Relationship Target=\"/Metadata/plate_1_small.png"
|
||||
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
|
||||
if (plate_small_thumbnail_written)
|
||||
stream << "<Relationship Target=\"/Metadata/plate_1_small.png"
|
||||
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
|
||||
} else {
|
||||
stream << " <Relationship Target=\"/" << xml_escape(data._3mf_printer_thumbnail_small)
|
||||
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (plate_thumbnail_written) {
|
||||
//always use plate thumbnails
|
||||
std::string thumbnail_file_str = (boost::format("Metadata/plate_%1%.png") % (export_plate_idx + 1)).str();
|
||||
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
|
||||
@@ -7079,9 +7098,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
|
||||
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
|
||||
|
||||
thumbnail_file_str = (boost::format("Metadata/plate_%1%_small.png") % (export_plate_idx + 1)).str();
|
||||
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
|
||||
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
|
||||
if (plate_small_thumbnail_written) {
|
||||
thumbnail_file_str = (boost::format("Metadata/plate_%1%_small.png") % (export_plate_idx + 1)).str();
|
||||
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
|
||||
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (targets.empty()) {
|
||||
@@ -7390,7 +7411,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// instance_id is just a 1 indexed index in build_items.
|
||||
//assert(m_skip_static || curr_id == build_items.size() + 1);
|
||||
|
||||
build_items.emplace_back("", object_it->second.object_id, t, instance->printable, instance->auto_drop);
|
||||
build_items.emplace_back("", object_it->second.object_id, t, instance->printable);
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -7840,8 +7861,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
stream << "\" " << PPATH_ATTR << "=\"" << xml_escape(item.path);
|
||||
stream << "\" " << TRANSFORM_ATTR << "=\"";
|
||||
add_transformation(stream, item.transform);
|
||||
stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable;
|
||||
stream << "\" " << AUTO_DROP_ATTR << "=\"" << item.auto_drop << "\"/>\n";
|
||||
stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable << "\"/>\n";
|
||||
}
|
||||
|
||||
stream << " </" << BUILD_TAG << ">\n";
|
||||
@@ -8278,7 +8298,8 @@ bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archiv
|
||||
if (obj != nullptr) {
|
||||
// Output of instances count added because of github #3435, currently not used by PrusaSlicer
|
||||
//stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << obj_metadata.first << "\" " << INSTANCESCOUNT_ATTR << "=\"" << obj->instances.size() << "\">\n";
|
||||
stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << object_data.object_id << "\">\n";
|
||||
stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << object_data.object_id
|
||||
<< "\" " << AUTO_DROP_ATTR << "=\"" << obj->auto_drop << "\">\n";
|
||||
|
||||
// stores object's name
|
||||
if (!obj->name.empty())
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../libslic3r.h"
|
||||
#include "../Model.hpp"
|
||||
#include "../TriangleMesh.hpp"
|
||||
#include "../I18N.hpp"
|
||||
|
||||
#include "svg.hpp"
|
||||
#include "nanosvg/nanosvg.h"
|
||||
@@ -143,11 +144,11 @@ bool get_svg_profile(const char *path, std::vector<Element_Info> &element_infos,
|
||||
NSVGimage *svg_data = nullptr;
|
||||
svg_data = nsvgParseFromFile(path, "mm", 96.0f);
|
||||
if (svg_data == nullptr) {
|
||||
message = "import svg failed: could not open svg.";
|
||||
message = _u8L("import svg failed: could not open svg.");
|
||||
return false;
|
||||
}
|
||||
if (svg_data->shapes == nullptr) {
|
||||
message = "import svg failed: could not parse imported svg data.";
|
||||
message = _u8L("import svg failed: could not parse imported svg data.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+13
-9
@@ -1979,7 +1979,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) {
|
||||
if (m_layer_idx < (int) m_tool_changes.size()) {
|
||||
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
|
||||
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
|
||||
throw Slic3r::RuntimeError(_(L("Wipe tower generation failed, possibly due to empty first layer.")));
|
||||
|
||||
// Calculate where the wipe tower layer will be printed. -1 means that print z will not change,
|
||||
// resulting in a wipe tower with sparse layers.
|
||||
@@ -2018,7 +2018,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) {
|
||||
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
|
||||
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
|
||||
throw Slic3r::RuntimeError(_(L("Wipe tower generation failed, possibly due to empty first layer.")));
|
||||
|
||||
if (!ignore_sparse)
|
||||
gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z);
|
||||
@@ -2044,7 +2044,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) {
|
||||
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
|
||||
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
|
||||
throw Slic3r::RuntimeError(_(L("Wipe tower generation failed, possibly due to empty first layer.")));
|
||||
|
||||
if (!ignore_sparse) {
|
||||
return false;
|
||||
@@ -2792,11 +2792,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
m_processor.set_print(print);
|
||||
GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor);
|
||||
if (! file.is_open()) {
|
||||
std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n";
|
||||
// TRN %1% is a file path.
|
||||
std::string err_msg = Slic3r::format(_(L("G-code export to %1% failed.\nCannot open the file for writing.")), path) + "\n";
|
||||
BOOST_LOG_TRIVIAL(error) << err_msg << std::endl;
|
||||
if (!fs::exists(folder)) {
|
||||
//fs::create_directory(folder);
|
||||
std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!";
|
||||
// TRN %1% is a folder path.
|
||||
std::string add_err_msg = Slic3r::format(_(L("The folder %1% does not exist.")), folder.string());
|
||||
BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl;
|
||||
err_msg += add_err_msg;
|
||||
}
|
||||
@@ -2818,7 +2820,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
if (file.is_error()) {
|
||||
file.close();
|
||||
boost::nowide::remove(path_tmp.c_str());
|
||||
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n");
|
||||
// TRN %1% is a file path.
|
||||
throw Slic3r::RuntimeError(Slic3r::format(_(L("G-code export to %1% failed.\nIs the disk full?")), path) + "\n");
|
||||
}
|
||||
} catch (std::exception &ex) {
|
||||
// Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown.
|
||||
@@ -2949,8 +2952,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
throw Slic3r::RuntimeError(
|
||||
std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' +
|
||||
"Is " + path_tmp + " locked?" + '\n');
|
||||
// TRN %1% is the temporary file path, %2% the output file path, %3% the system error message.
|
||||
Slic3r::format(_(L("Failed to rename the output G-code file from %1% to %2%.\nError: %3%\nIs %1% locked?")),
|
||||
path_tmp, path, ret.message()) + '\n');
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("rename_file from %1% to %2% successfully")% path_tmp % path;
|
||||
@@ -4161,7 +4165,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
|
||||
|
||||
if (errors != enum_bitmask<ThumbnailError>()) {
|
||||
std::string error_str = format("Invalid thumbnails value:");
|
||||
std::string error_str = _(L("Invalid thumbnails value:"));
|
||||
error_str += GCodeThumbnails::get_error_string(errors);
|
||||
throw Slic3r::ExportError(error_str);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/GCodeWriter.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -321,8 +323,7 @@ std::string AdaptivePAProcessor::validate_adaptive_pa_model(const std::string& m
|
||||
// Only numbers, commas and dots are allowed (no letters or other characters)
|
||||
for (char c : line) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(c)) && c != ',' && c != '.') {
|
||||
return "Line " + std::to_string(line_number) +
|
||||
": only numbers, commas and dots are allowed";
|
||||
return Slic3r::format(_u8L("Line %1%: only numbers, commas and dots are allowed"), line_number);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,8 +334,7 @@ std::string AdaptivePAProcessor::validate_adaptive_pa_model(const std::string& m
|
||||
}
|
||||
|
||||
if (comma_count != 2) {
|
||||
return "Line " + std::to_string(line_number) +
|
||||
": must contain exactly 3 comma-separated values (PA, flow, acceleration)";
|
||||
return Slic3r::format(_u8L("Line %1%: must contain exactly 3 comma-separated values (PA, flow, acceleration)"), line_number);
|
||||
}
|
||||
|
||||
// Parse and validate the values
|
||||
@@ -344,31 +344,31 @@ std::string AdaptivePAProcessor::validate_adaptive_pa_model(const std::string& m
|
||||
|
||||
// Parse PA
|
||||
if (!std::getline(line_stream, value, ','))
|
||||
return "Line " + std::to_string(line_number) + ": missing PA value";
|
||||
return Slic3r::format(_u8L("Line %1%: missing PA value"), line_number);
|
||||
double pa = std::stod(value);
|
||||
|
||||
// Parse flow
|
||||
if (!std::getline(line_stream, value, ','))
|
||||
return "Line " + std::to_string(line_number) + ": missing flow value";
|
||||
return Slic3r::format(_u8L("Line %1%: missing flow value"), line_number);
|
||||
double flow = std::stod(value);
|
||||
|
||||
// Parse acceleration
|
||||
if (!std::getline(line_stream, value, ','))
|
||||
return "Line " + std::to_string(line_number) + ": missing acceleration value";
|
||||
return Slic3r::format(_u8L("Line %1%: missing acceleration value"), line_number);
|
||||
double accel = std::stod(value);
|
||||
|
||||
// Validate constraints
|
||||
if (pa >= 2.0) {
|
||||
return "Line " + std::to_string(line_number) + ": PA value must be less than 2";
|
||||
return Slic3r::format(_u8L("Line %1%: PA value must be less than 2"), line_number);
|
||||
}
|
||||
if (flow <= pa) {
|
||||
return "Line " + std::to_string(line_number) + ": flow value must be greater than PA value";
|
||||
return Slic3r::format(_u8L("Line %1%: flow value must be greater than PA value"), line_number);
|
||||
}
|
||||
if (accel <= flow) {
|
||||
return "Line " + std::to_string(line_number) + ": acceleration value must be greater than flow value";
|
||||
return Slic3r::format(_u8L("Line %1%: acceleration value must be greater than flow value"), line_number);
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
return "Line " + std::to_string(line_number) + ": invalid numeric value";
|
||||
return Slic3r::format(_u8L("Line %1%: invalid numeric value"), line_number);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/LocalesUtils.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
#include "GCodeProcessor.hpp"
|
||||
|
||||
@@ -1066,13 +1067,13 @@ void GCodeProcessor::run_post_process()
|
||||
{
|
||||
FilePtr in{ boost::nowide::fopen(m_result.filename.c_str(), "rb") };
|
||||
if (in.f == nullptr)
|
||||
throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for reading.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("G-code processor post-process export failed.\nCannot open file for reading.\n"));
|
||||
|
||||
// temporary file to contain modified gcode
|
||||
std::string out_path = m_result.filename + ".postprocess";
|
||||
FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") };
|
||||
if (out.f == nullptr)
|
||||
throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for writing.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("G-code processor post-process export failed.\nCannot open file for writing.\n"));
|
||||
|
||||
std::vector<double> filament_mm(m_result.filaments_count, 0.0);
|
||||
std::vector<double> filament_cm3(m_result.filaments_count, 0.0);
|
||||
@@ -1467,10 +1468,10 @@ void GCodeProcessor::run_post_process()
|
||||
// found an invalid value, clamp it to a valid one
|
||||
tool_number = std::clamp<int>(0, m_filament_nozzle_temp.size() - 1, tool_number);
|
||||
// emit warning
|
||||
std::string warning = "GCode Post-Processor encountered an invalid toolchange, maybe from a custom gcode:";
|
||||
std::string warning = _u8L("The G-code post-processor encountered an invalid tool change, maybe from custom G-code:");
|
||||
warning += "\n> ";
|
||||
warning += gcode_line;
|
||||
warning += "Generated M104 lines may be incorrect.";
|
||||
warning += _u8L("Generated M104 lines may be incorrect.");
|
||||
BOOST_LOG_TRIVIAL(error) << warning;
|
||||
// Orca todo
|
||||
if (m_print != nullptr)
|
||||
@@ -1712,7 +1713,7 @@ void GCodeProcessor::run_post_process()
|
||||
for (;;) {
|
||||
size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f);
|
||||
if (::ferror(in.f))
|
||||
throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nError while reading from file.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("G-code processor post-process export failed.\nError while reading from file.\n"));
|
||||
bool eof = cnt_read == 0;
|
||||
auto it = buffer.begin();
|
||||
auto it_bufend = buffer.begin() + cnt_read;
|
||||
@@ -1877,8 +1878,9 @@ void GCodeProcessor::run_post_process()
|
||||
}
|
||||
|
||||
if (rename_file(out_path, result_filename))
|
||||
throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + result_filename + '\n' +
|
||||
"Is " + out_path + " locked?" + '\n');
|
||||
// TRN %1% is the temporary file path, %2% the output file path.
|
||||
throw Slic3r::RuntimeError(Slic3r::format(_u8L("Failed to rename the output G-code file from %1% to %2%.\nIs %1% locked?"),
|
||||
out_path, result_filename) + '\n');
|
||||
}
|
||||
|
||||
// Additive second file-rewrite pass for the pre-heat/pre-cool injector. The single-pass
|
||||
@@ -1979,12 +1981,12 @@ void GCodeProcessor::run_second_pass_injection()
|
||||
|
||||
FilePtr in{ boost::nowide::fopen(m_result.filename.c_str(), "rb") };
|
||||
if (in.f == nullptr)
|
||||
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nCannot open file for reading.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("G-code processor pre-heat injection pass failed.\nCannot open file for reading.\n"));
|
||||
|
||||
const std::string out_path = m_result.filename + ".preheat";
|
||||
FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") };
|
||||
if (out.f == nullptr)
|
||||
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nCannot open file for writing.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("G-code processor pre-heat injection pass failed.\nCannot open file for writing.\n"));
|
||||
|
||||
// The rewrite may shift byte positions (once the injector inserts lines), so rebuild lines_ends from scratch.
|
||||
// With an empty map the scanned '\n' offsets reproduce the current lines_ends exactly.
|
||||
@@ -2002,7 +2004,7 @@ void GCodeProcessor::run_second_pass_injection()
|
||||
for (;;) {
|
||||
size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f);
|
||||
if (::ferror(in.f))
|
||||
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nError while reading from file.\n"));
|
||||
throw Slic3r::RuntimeError(_u8L("G-code processor pre-heat injection pass failed.\nError while reading from file.\n"));
|
||||
bool eof = cnt_read == 0;
|
||||
auto it = buffer.begin();
|
||||
auto it_bufend = buffer.begin() + cnt_read;
|
||||
@@ -2053,8 +2055,9 @@ void GCodeProcessor::run_second_pass_injection()
|
||||
handle_offsets_of_second_process(inserted_operation_lines);
|
||||
|
||||
if (rename_file(out_path, m_result.filename))
|
||||
throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + m_result.filename + '\n' +
|
||||
"Is " + out_path + " locked?" + '\n');
|
||||
// TRN %1% is the temporary file path, %2% the output file path.
|
||||
throw Slic3r::RuntimeError(Slic3r::format(_u8L("Failed to rename the output G-code file from %1% to %2%.\nIs %1% locked?"),
|
||||
out_path, m_result.filename) + '\n');
|
||||
}
|
||||
|
||||
// Every move whose gcode_id sits at or after an inserted line is pushed down by the running count of
|
||||
@@ -3727,6 +3730,7 @@ void GCodeProcessor::reset()
|
||||
m_detect_layer_based_on_tag = false;
|
||||
|
||||
m_seams_count = 0;
|
||||
m_g29_time_added = false;
|
||||
m_preheat_time = 0.f;
|
||||
m_preheat_steps = 1;
|
||||
}
|
||||
@@ -3748,7 +3752,8 @@ DynamicConfig GCodeProcessor::export_config_for_render() const
|
||||
config.set_key_value("filament_is_support", new ConfigOptionBools(m_parser.get_config().filament_is_support.values));
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings(m_parser.get_config().filament_type.values));
|
||||
config.set_key_value("filament_map", new ConfigOptionInts(m_parser.get_config().filament_map.values));
|
||||
config.apply(m_belt_render_config);
|
||||
// += clones the options; apply() would need a definition this config does not have.
|
||||
config += m_belt_render_config;
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -3941,7 +3946,7 @@ ConfigSubstitutions load_from_superslicer_gcode_file(const std::string& filename
|
||||
key_value_pairs = ConfigBase::load_from_gcode_string_legacy(config, data.data(), substitutions_ctxt);
|
||||
|
||||
if (key_value_pairs < 80)
|
||||
throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", filename, key_value_pairs));
|
||||
throw Slic3r::RuntimeError(format(_u8L("Suspiciously low number of configuration values extracted from %1%: %2%"), filename, key_value_pairs));
|
||||
|
||||
return std::move(substitutions_ctxt.substitutions);
|
||||
}
|
||||
@@ -6128,15 +6133,12 @@ void GCodeProcessor::process_G4(const GCodeReader::GCodeLine& line)
|
||||
//BBS
|
||||
void GCodeProcessor::process_G29(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
//BBS: hardcode 260 seconds for G29
|
||||
//Todo: use a machine related setting when we have second kind of BBL printer
|
||||
const float value_s = 260.0;
|
||||
if (s_IsBBLPrinter){
|
||||
if(m_measure_g29_time)
|
||||
simulate_st_synchronize(value_s);
|
||||
}
|
||||
else{
|
||||
simulate_st_synchronize(value_s);
|
||||
// Firmware may split one bed-leveling sequence across multiple G29 subcommands.
|
||||
constexpr float g29_time = 260.0f;
|
||||
const bool should_add_time = !s_IsBBLPrinter || m_measure_g29_time;
|
||||
if (should_add_time && !m_g29_time_added) {
|
||||
simulate_st_synchronize(g29_time);
|
||||
m_g29_time_added = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1280,6 +1280,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
bool m_detect_layer_based_on_tag {false};
|
||||
int m_seams_count;
|
||||
bool m_measure_g29_time {false};
|
||||
bool m_g29_time_added {false};
|
||||
bool m_single_extruder_multi_material;
|
||||
float m_preheat_time;
|
||||
int m_preheat_steps;
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/GCode/PchipInterpolatorHelper.hpp"
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
|
||||
#include "SmallAreaInfillFlowCompensator.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -55,10 +57,8 @@ SmallAreaInfillFlowCompensator::SmallAreaInfillFlowCompensator(const Slic3r::GCo
|
||||
flowComps.push_back(std::stod(value_str));
|
||||
}
|
||||
} catch (...) {
|
||||
std::stringstream ss;
|
||||
ss << "Small Area Flow Compensation: Error parsing data point in small area infill compensation model:" << line << std::endl;
|
||||
|
||||
throw Slic3r::InvalidArgument(ss.str());
|
||||
// TRN %1% is the line of the model that could not be parsed.
|
||||
throw Slic3r::InvalidArgument(Slic3r::format(_u8L("Small Area Flow Compensation: Error parsing data point in small area infill compensation model: %1%"), line) + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,26 +66,26 @@ SmallAreaInfillFlowCompensator::SmallAreaInfillFlowCompensator(const Slic3r::GCo
|
||||
for (size_t i = 0; i < eLengths.size(); i++) {
|
||||
if (i == 0) {
|
||||
if (!nearly_equal(eLengths[i], 0.0)) {
|
||||
throw Slic3r::InvalidArgument("Small Area Flow Compensation: First extrusion length for small area infill compensation model must be 0");
|
||||
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: First extrusion length for small area infill compensation model must be 0"));
|
||||
}
|
||||
} else {
|
||||
if (nearly_equal(eLengths[i], 0.0)) {
|
||||
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Only the first extrusion length for small area infill compensation model can be 0");
|
||||
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Only the first extrusion length for small area infill compensation model can be 0"));
|
||||
}
|
||||
if (eLengths[i] <= eLengths[i - 1]) {
|
||||
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Extrusion lengths for subsequent points must be increasing");
|
||||
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Extrusion lengths for subsequent points must be increasing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 1; i < flowComps.size(); ++i) {
|
||||
if (flowComps[i] <= flowComps[i - 1]) {
|
||||
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Flow compensation factors must strictly increase with extrusion length");
|
||||
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Flow compensation factors must strictly increase with extrusion length"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!flowComps.empty() && !nearly_equal(flowComps.back(), 1.0)) {
|
||||
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Final compensation factor for small area infill flow compensation model must be 1.0");
|
||||
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Final compensation factor for small area infill flow compensation model must be 1.0"));
|
||||
}
|
||||
|
||||
flowModel = std::make_unique<PchipInterpolatorHelper>(eLengths, flowComps);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/enum_bitmask.hpp"
|
||||
#include "format.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <qoi/qoi.h>
|
||||
@@ -610,11 +611,11 @@ std::string get_error_string(const ThumbnailErrors& errors)
|
||||
std::string error_str;
|
||||
|
||||
if (errors.has(ThumbnailError::InvalidVal))
|
||||
error_str += "\n - " + Slic3r::format("Invalid input format. Expected vector of dimensions in the following format: \"%1%\"", "XxY/EXT, XxY/EXT, ...");
|
||||
error_str += "\n - " + Slic3r::format(_u8L("Invalid input format. Expected vector of dimensions in the following format: \"%1%\""), "XxY/EXT, XxY/EXT, ...");
|
||||
if (errors.has(ThumbnailError::OutOfRange))
|
||||
error_str += "\n - Input value is out of range";
|
||||
error_str += "\n - " + _u8L("Input value is out of range");
|
||||
if (errors.has(ThumbnailError::InvalidExt))
|
||||
error_str += "\n - Some extension in the input is invalid";
|
||||
error_str += "\n - " + _u8L("Some extension in the input is invalid");
|
||||
|
||||
return error_str;
|
||||
}
|
||||
|
||||
@@ -1646,7 +1646,7 @@ static FilamentGroupContext build_filament_group_context(
|
||||
context.nozzle_info.extruder_nozzle_list = build_extruder_nozzle_list(context.nozzle_info.nozzle_list);
|
||||
|
||||
if (context.nozzle_info.nozzle_list.empty())
|
||||
throw Slic3r::RuntimeError("No valid nozzle found. Please check nozzle count.");
|
||||
throw Slic3r::RuntimeError(_L("No valid nozzle found. Please check nozzle count."));
|
||||
|
||||
if (!nozzle_status.empty())
|
||||
context.nozzle_info.nozzle_status = nozzle_status;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "WipeTower.hpp"
|
||||
#include "WipeTower2.hpp"
|
||||
#include "../Config.hpp"
|
||||
#include "../FilamentMixer.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../libslic3r.h"
|
||||
|
||||
@@ -74,10 +75,19 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo
|
||||
return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.);
|
||||
}
|
||||
|
||||
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_ids, double layer_height, double max_object_height)
|
||||
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_slots, double layer_height, double max_object_height)
|
||||
{
|
||||
WipeTowerFootprint footprint;
|
||||
footprint.height = max_object_height;
|
||||
// A mixed slot is virtual: the tool ordering resolves it to one of its physical components
|
||||
// on every layer, with or without colour sublayers, so the tower only ever purges those.
|
||||
// Validation passes the slots as Print::extruders() lists them; the GUI and CLI placement
|
||||
// expand them beforehand, which repeating here leaves unchanged.
|
||||
std::vector<unsigned int> filament_ids = filament_slots;
|
||||
const auto *is_mixed_opt = dynamic_cast<const ConfigOptionBools *>(option_of(config, "filament_is_mixed"));
|
||||
const auto *components_opt = dynamic_cast<const ConfigOptionStrings *>(option_of(config, "filament_mixed_components"));
|
||||
if (is_mixed_opt != nullptr && components_opt != nullptr && has_any_mixed_filament(is_mixed_opt->values))
|
||||
filament_ids = expand_mixed_filaments(filament_slots, is_mixed_opt->values, components_opt->values);
|
||||
const size_t filaments_cnt = filament_ids.size();
|
||||
if (filaments_cnt == 0 || layer_height < EPSILON)
|
||||
return footprint;
|
||||
|
||||
@@ -34,6 +34,8 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo
|
||||
// filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool
|
||||
// changes, so ids derived from the model must include them
|
||||
// (Print::extruders(true)) or a real tower is sized as if it were never built.
|
||||
// A mixed slot may be passed as itself or already expanded: it is sized as its
|
||||
// physical components either way, since those are all the tower ever purges.
|
||||
// layer_height: thinnest layer the objects are sliced at. The first layer is folded in here.
|
||||
//
|
||||
// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate
|
||||
|
||||
@@ -198,10 +198,12 @@ std::string imex_multicolor_block_reason(const std::string& parallel_mode,
|
||||
// Filaments are numbered from 1 as the sidebar shows them, heads by their T index; the map
|
||||
// decides which head a filament is on, so with MMU lanes the two numbers differ.
|
||||
if (phys < 0)
|
||||
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% isn't routed to any "
|
||||
// TRN %1% is a list of tools such as "T0/T1", %2% a filament number.
|
||||
return (boost::format(L("In this Span mode, colors are printed only by %1%, but filament %2% isn't routed to any "
|
||||
"tool. Use filaments routed to %1%, or switch this plate to Primary mode.")) %
|
||||
tools % (filament + 1)).str();
|
||||
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% is routed to T%3%. Use "
|
||||
// TRN %1% is a list of tools such as "T0/T1", %2% a filament number, %3% a tool number.
|
||||
return (boost::format(L("In this Span mode, colors are printed only by %1%, but filament %2% is routed to T%3%. Use "
|
||||
"filaments routed to %1%, or switch this plate to Primary mode.")) %
|
||||
tools % (filament + 1) % phys).str();
|
||||
}
|
||||
|
||||
@@ -472,7 +472,7 @@ Model Model::read_from_file(const std::string&
|
||||
boost::algorithm::iends_with(input_file, ".abc") || boost::algorithm::iends_with(input_file, ".ply")) {
|
||||
std::string temp_stl = make_temp_stl_with_modelio(input_file);
|
||||
if (temp_stl.empty()) {
|
||||
throw Slic3r::RuntimeError("Failed to convert asset to STL via ModelIO.");
|
||||
throw Slic3r::RuntimeError(_L("Failed to convert asset to STL via ModelIO."));
|
||||
}
|
||||
result = load_stl(temp_stl.c_str(), &model);
|
||||
delete_temp_file(temp_stl);
|
||||
@@ -1280,6 +1280,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
|
||||
this->layer_config_ranges = rhs.layer_config_ranges;
|
||||
this->layer_height_profile = rhs.layer_height_profile;
|
||||
this->printable = rhs.printable;
|
||||
this->auto_drop = rhs.auto_drop;
|
||||
this->origin_translation = rhs.origin_translation;
|
||||
this->cut_id.copy(rhs.cut_id);
|
||||
this->copy_transformation_caches(rhs);
|
||||
@@ -1320,6 +1321,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
|
||||
this->layer_config_ranges = std::move(rhs.layer_config_ranges);
|
||||
this->layer_height_profile = std::move(rhs.layer_height_profile);
|
||||
this->printable = std::move(rhs.printable);
|
||||
this->auto_drop = rhs.auto_drop;
|
||||
this->origin_translation = std::move(rhs.origin_translation);
|
||||
this->copy_transformation_caches(rhs);
|
||||
|
||||
@@ -1889,6 +1891,9 @@ void ModelObject::center_around_origin(bool include_modifiers)
|
||||
|
||||
void ModelObject::ensure_on_bed(bool allow_negative_z)
|
||||
{
|
||||
if (!auto_drop)
|
||||
return;
|
||||
|
||||
double z_offset = 0.0;
|
||||
|
||||
if (allow_negative_z) {
|
||||
@@ -1907,14 +1912,8 @@ void ModelObject::ensure_on_bed(bool allow_negative_z)
|
||||
else
|
||||
z_offset = -this->min_z();
|
||||
|
||||
if (z_offset != 0.0) {
|
||||
for (size_t i = 0; i < instances.size(); ++i) {
|
||||
if (!instances[i]->auto_drop)
|
||||
continue;
|
||||
|
||||
translate_instance(i, z_offset * Vec3d::UnitZ());
|
||||
}
|
||||
}
|
||||
if (z_offset != 0.0)
|
||||
translate_instances(z_offset * Vec3d::UnitZ());
|
||||
}
|
||||
|
||||
void ModelObject::translate_instances(const Vec3d& vector)
|
||||
|
||||
@@ -400,6 +400,9 @@ public:
|
||||
LayerHeightProfile layer_height_profile;
|
||||
// Whether or not this object is printable
|
||||
bool printable { true };
|
||||
// Whether this object is snapped to the build plate after transformations.
|
||||
// Instance Z offsets are shared by all instances of an object.
|
||||
bool auto_drop { true };
|
||||
|
||||
// This vector holds position of selected support points for SLA. The data are
|
||||
// saved in mesh coordinates to allow using them for several instances.
|
||||
@@ -706,7 +709,7 @@ private:
|
||||
Internal::StaticSerializationWrapper<ModelConfigObject const> config_wrapper(config);
|
||||
Internal::StaticSerializationWrapper<LayerHeightProfile const> layer_heigth_profile_wrapper(layer_height_profile);
|
||||
ar(name, module_name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper,
|
||||
sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, brim_points,
|
||||
sla_support_points, sla_points_status, sla_drain_holes, printable, auto_drop, origin_translation, brim_points,
|
||||
m_bounding_box_approx, m_bounding_box_approx_valid,
|
||||
m_bounding_box_exact, m_bounding_box_exact_valid, m_min_max_z_valid,
|
||||
m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid,
|
||||
@@ -719,7 +722,7 @@ private:
|
||||
// BBS: add backup, check modify
|
||||
SaveObjectGaurd gaurd(*this);
|
||||
ar(name, module_name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper,
|
||||
sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, brim_points,
|
||||
sla_support_points, sla_points_status, sla_drain_holes, printable, auto_drop, origin_translation, brim_points,
|
||||
m_bounding_box_approx, m_bounding_box_approx_valid,
|
||||
m_bounding_box_exact, m_bounding_box_exact_valid, m_min_max_z_valid,
|
||||
m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid,
|
||||
@@ -1427,7 +1430,6 @@ public:
|
||||
ModelInstanceEPrintVolumeState print_volume_state;
|
||||
// Whether or not this instance is printable
|
||||
bool printable;
|
||||
bool auto_drop;
|
||||
bool use_loaded_id_for_label {false};
|
||||
int arrange_order = 0; // BBS
|
||||
size_t loaded_id = 0; // BBS
|
||||
@@ -1557,7 +1559,7 @@ private:
|
||||
|
||||
// Constructor, which assigns a new unique ID.
|
||||
explicit ModelInstance(ModelObject* object)
|
||||
: print_volume_state(ModelInstancePVS_Inside), printable(true), auto_drop(true), object(object), m_assemble_initialized(false)
|
||||
: print_volume_state(ModelInstancePVS_Inside), printable(true), object(object), m_assemble_initialized(false)
|
||||
{
|
||||
assert(this->id().valid());
|
||||
}
|
||||
@@ -1568,7 +1570,6 @@ private:
|
||||
, m_offset_to_assembly(other.m_offset_to_assembly)
|
||||
, print_volume_state(ModelInstancePVS_Inside)
|
||||
, printable(other.printable)
|
||||
, auto_drop(other.auto_drop)
|
||||
, object(object)
|
||||
, m_assemble_initialized(false) { assert(this->id().valid() && this->id() != other.id()); }
|
||||
|
||||
@@ -1582,7 +1583,7 @@ private:
|
||||
ModelInstance() : ObjectBase(-1), object(nullptr) { assert(this->id().invalid()); }
|
||||
// BBS. Add added members to archive.
|
||||
template<class Archive> void serialize(Archive& ar) {
|
||||
ar(m_transformation, print_volume_state, printable, auto_drop, m_assemble_transformation, m_offset_to_assembly, m_assemble_initialized);
|
||||
ar(m_transformation, print_volume_state, printable, m_assemble_transformation, m_offset_to_assembly, m_assemble_initialized);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+133
-132
@@ -5,6 +5,8 @@
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
#include <cassert>
|
||||
#include <boost/range/iterator_range_core.hpp>
|
||||
@@ -383,7 +385,7 @@ namespace client
|
||||
case TYPE_DOUBLE:
|
||||
return expr(- this->d(), start_pos, this->it_range.end());
|
||||
default:
|
||||
this->throw_exception("Cannot apply unary minus operator.");
|
||||
this->throw_exception(L("Cannot apply unary minus operator."));
|
||||
}
|
||||
assert(false);
|
||||
// Suppress compiler warnings.
|
||||
@@ -401,7 +403,7 @@ namespace client
|
||||
case TYPE_DOUBLE:
|
||||
return expr(static_cast<int>(this->d()), start_pos, this->it_range.end());
|
||||
default:
|
||||
this->throw_exception("Cannot convert to integer.");
|
||||
this->throw_exception(L("Cannot convert to integer."));
|
||||
}
|
||||
assert(false);
|
||||
// Suppress compiler warnings.
|
||||
@@ -419,7 +421,7 @@ namespace client
|
||||
case TYPE_DOUBLE:
|
||||
return expr(static_cast<int>(std::round(this->d())), start_pos, this->it_range.end());
|
||||
default:
|
||||
this->throw_exception("Cannot round a non-numeric value.");
|
||||
this->throw_exception(L("Cannot round a non-numeric value."));
|
||||
}
|
||||
assert(false);
|
||||
// Suppress compiler warnings.
|
||||
@@ -434,7 +436,7 @@ namespace client
|
||||
case TYPE_DOUBLE:
|
||||
return expr(static_cast<int>(std::floor(this->d())), start_pos, this->it_range.end());
|
||||
default:
|
||||
this->throw_exception("Cannot floor a non-numeric value.");
|
||||
this->throw_exception(L("Cannot floor a non-numeric value."));
|
||||
}
|
||||
assert(false);
|
||||
// Suppress compiler warnings.
|
||||
@@ -449,7 +451,7 @@ namespace client
|
||||
case TYPE_DOUBLE:
|
||||
return expr(static_cast<int>(std::ceil(this->d())), start_pos, this->it_range.end());
|
||||
default:
|
||||
this->throw_exception("Cannot ceil a non-numeric value.");
|
||||
this->throw_exception(L("Cannot ceil a non-numeric value."));
|
||||
}
|
||||
assert(false);
|
||||
// Suppress compiler warnings.
|
||||
@@ -465,7 +467,7 @@ namespace client
|
||||
case TYPE_BOOL:
|
||||
return expr(! this->b(), start_pos, this->it_range.end());
|
||||
default:
|
||||
this->throw_exception("Cannot apply a not operator.");
|
||||
this->throw_exception(L("Cannot apply a not operator."));
|
||||
}
|
||||
assert(false);
|
||||
// Suppress compiler warnings.
|
||||
@@ -483,7 +485,7 @@ namespace client
|
||||
// Conver the left hand side to string, append rhs.
|
||||
this->set_s(this->to_string() + rhs.s());
|
||||
} else {
|
||||
const char *err_msg = "Cannot add non-numeric types.";
|
||||
const char *err_msg = L("Cannot add non-numeric types.");
|
||||
this->throw_if_not_numeric(err_msg);
|
||||
rhs.throw_if_not_numeric(err_msg);
|
||||
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
||||
@@ -501,7 +503,7 @@ namespace client
|
||||
// Inside an if / else block to be skipped.
|
||||
this->reset();
|
||||
} else {
|
||||
const char *err_msg = "Cannot subtract non-numeric types.";
|
||||
const char *err_msg = L("Cannot subtract non-numeric types.");
|
||||
this->throw_if_not_numeric(err_msg);
|
||||
rhs.throw_if_not_numeric(err_msg);
|
||||
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
||||
@@ -519,7 +521,7 @@ namespace client
|
||||
// Inside an if / else block to be skipped.
|
||||
this->reset();
|
||||
} else {
|
||||
const char *err_msg = "Cannot multiply with non-numeric type.";
|
||||
const char *err_msg = L("Cannot multiply with non-numeric type.");
|
||||
this->throw_if_not_numeric(err_msg);
|
||||
rhs.throw_if_not_numeric(err_msg);
|
||||
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
||||
@@ -537,10 +539,10 @@ namespace client
|
||||
// Inside an if / else block to be skipped.
|
||||
this->reset();
|
||||
} else {
|
||||
this->throw_if_not_numeric("Cannot divide a non-numeric type.");
|
||||
rhs.throw_if_not_numeric("Cannot divide with a non-numeric type.");
|
||||
this->throw_if_not_numeric(L("Cannot divide a non-numeric type."));
|
||||
rhs.throw_if_not_numeric(L("Cannot divide with a non-numeric type."));
|
||||
if (rhs.type() == TYPE_INT ? (rhs.i() == 0) : (rhs.d() == 0.))
|
||||
rhs.throw_exception("Division by zero");
|
||||
rhs.throw_exception(L("Division by zero"));
|
||||
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
||||
this->set_d_lite(this->as_d() / rhs.as_d());
|
||||
else
|
||||
@@ -556,10 +558,10 @@ namespace client
|
||||
// Inside an if / else block to be skipped.
|
||||
this->reset();
|
||||
} else {
|
||||
this->throw_if_not_numeric("Cannot divide a non-numeric type.");
|
||||
rhs.throw_if_not_numeric("Cannot divide with a non-numeric type.");
|
||||
this->throw_if_not_numeric(L("Cannot divide a non-numeric type."));
|
||||
rhs.throw_if_not_numeric(L("Cannot divide with a non-numeric type."));
|
||||
if (rhs.type() == TYPE_INT ? (rhs.i() == 0) : (rhs.d() == 0.))
|
||||
rhs.throw_exception("Division by zero");
|
||||
rhs.throw_exception(L("Division by zero"));
|
||||
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
||||
this->set_d_lite(std::fmod(this->as_d(), rhs.as_d()));
|
||||
else
|
||||
@@ -581,7 +583,7 @@ namespace client
|
||||
if (self.type() != TYPE_EMPTY) {
|
||||
// Not inside an if / else block to be skipped
|
||||
if (self.type() != TYPE_BOOL)
|
||||
self.throw_exception("Not a boolean expression");
|
||||
self.throw_exception(L("Not a boolean expression"));
|
||||
out = self.b();
|
||||
}
|
||||
}
|
||||
@@ -590,7 +592,7 @@ namespace client
|
||||
{
|
||||
assert(self.type() != TYPE_EMPTY);
|
||||
if (self.type() != TYPE_BOOL)
|
||||
self.throw_exception("Not a boolean expression");
|
||||
self.throw_exception(L("Not a boolean expression"));
|
||||
out = self.b() ? "true" : "false";
|
||||
}
|
||||
|
||||
@@ -622,7 +624,7 @@ namespace client
|
||||
// Both type are bool.
|
||||
if (op != '=')
|
||||
boost::throw_exception(qi::expectation_failure<Iterator>(
|
||||
lhs.it_range.begin(), rhs.it_range.end(), spirit::info("*Cannot compare the types.")));
|
||||
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot compare the types."))));
|
||||
value = lhs.b() == rhs.b();
|
||||
} else if (lhs.type() == TYPE_STRING || rhs.type() == TYPE_STRING) {
|
||||
// One type is string, the other could be converted to string.
|
||||
@@ -630,7 +632,7 @@ namespace client
|
||||
(op == '<') ? (lhs.to_string() < rhs.to_string()) : (lhs.to_string() > rhs.to_string());
|
||||
} else {
|
||||
boost::throw_exception(qi::expectation_failure<Iterator>(
|
||||
lhs.it_range.begin(), rhs.it_range.end(), spirit::info("*Cannot compare the types.")));
|
||||
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot compare the types."))));
|
||||
}
|
||||
lhs.reset();
|
||||
lhs.set_b_lite(invert ? ! value : value);
|
||||
@@ -645,7 +647,7 @@ namespace client
|
||||
|
||||
static void throw_if_not_numeric(const expr ¶m)
|
||||
{
|
||||
const char *err_msg = "Not a numeric type.";
|
||||
const char *err_msg = L("Not a numeric type.");
|
||||
param.throw_if_not_numeric(err_msg);
|
||||
}
|
||||
|
||||
@@ -707,10 +709,10 @@ namespace client
|
||||
return;
|
||||
throw_if_not_numeric(param1);
|
||||
if (param2.type() != TYPE_INT)
|
||||
param2.throw_exception("digits: second parameter must be integer");
|
||||
param2.throw_exception(L("digits: second parameter must be integer"));
|
||||
bool has_decimals = param3.type() != TYPE_EMPTY;
|
||||
if (has_decimals && param3.type() != TYPE_INT)
|
||||
param3.throw_exception("digits: third parameter must be integer");
|
||||
param3.throw_exception(L("digits: third parameter must be integer"));
|
||||
|
||||
char buf[256];
|
||||
int ndigits = std::clamp(param2.as_i(), 0, 64);
|
||||
@@ -734,7 +736,7 @@ namespace client
|
||||
// One type is string, the other could be converted to string.
|
||||
subject = &lhs.s();
|
||||
} else {
|
||||
lhs.throw_exception("Left hand side of a regex match must be a string.");
|
||||
lhs.throw_exception(L("Left hand side of a regex match must be a string."));
|
||||
}
|
||||
try {
|
||||
std::string pattern(++ rhs.begin(), -- rhs.end());
|
||||
@@ -745,7 +747,7 @@ namespace client
|
||||
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
|
||||
// Syntax error in the regular expression
|
||||
boost::throw_exception(qi::expectation_failure<Iterator>(
|
||||
rhs.begin(), rhs.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what())));
|
||||
rhs.begin(), rhs.end(), spirit::info("*" + Slic3r::format(_u8L("Regular expression compilation failed: %1%"), ex.what()))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,7 +762,7 @@ namespace client
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
if (subject.type() != TYPE_STRING)
|
||||
subject.throw_exception("regex_replace() first parameter must be a string.");
|
||||
subject.throw_exception(L("regex_replace() first parameter must be a string."));
|
||||
try {
|
||||
std::string re(++ pattern.begin(), -- pattern.end());
|
||||
std::string result = SLIC3R_REGEX_NAMESPACE::regex_replace(subject.s(), SLIC3R_REGEX_NAMESPACE::regex(re), replacement.to_string());
|
||||
@@ -768,7 +770,7 @@ namespace client
|
||||
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
|
||||
// Syntax error in the regular expression
|
||||
boost::throw_exception(qi::expectation_failure<Iterator>(
|
||||
pattern.begin(), pattern.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what())));
|
||||
pattern.begin(), pattern.end(), spirit::info("*" + Slic3r::format(_u8L("Regular expression compilation failed: %1%"), ex.what()))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,15 +786,15 @@ namespace client
|
||||
}
|
||||
if (! out.b()) {
|
||||
if (match.type() != TYPE_STRING)
|
||||
match.throw_exception("one_of(): First parameter (the string to match against) has to be a string value");
|
||||
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
|
||||
if (pattern.type() != TYPE_STRING)
|
||||
match.throw_exception("one_of(): Pattern has to be a string value");
|
||||
match.throw_exception(L("one_of(): Pattern has to be a string value"));
|
||||
if (RegEx) {
|
||||
try {
|
||||
out.set_b(SLIC3R_REGEX_NAMESPACE::regex_match(match.s(), SLIC3R_REGEX_NAMESPACE::regex(pattern.s())));
|
||||
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &) {
|
||||
// Syntax error in the regular expression
|
||||
pattern.throw_exception("Regular expression compilation failed");
|
||||
pattern.throw_exception(L("Regular expression compilation failed"));
|
||||
}
|
||||
} else
|
||||
out.set_b(match.s() == pattern.s());
|
||||
@@ -806,7 +808,7 @@ namespace client
|
||||
}
|
||||
if (! out.b()) {
|
||||
if (match.type() != TYPE_STRING)
|
||||
match.throw_exception("one_of(): First parameter (the string to match against) has to be a string value");
|
||||
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
|
||||
regex_op(match, pattern, '=', out);
|
||||
}
|
||||
}
|
||||
@@ -821,7 +823,7 @@ namespace client
|
||||
value = (op == '|') ? (lhs.b() || rhs.b()) : (lhs.b() && rhs.b());
|
||||
} else {
|
||||
boost::throw_exception(qi::expectation_failure<Iterator>(
|
||||
lhs.it_range.begin(), rhs.it_range.end(), spirit::info("*Cannot apply logical operation to non-boolean operators.")));
|
||||
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot apply logical operation to non-boolean operands."))));
|
||||
}
|
||||
lhs.set_b_lite(value);
|
||||
}
|
||||
@@ -984,30 +986,30 @@ namespace client
|
||||
opt = ctx->resolve_symbol(opt_key_str.substr(0, idx));
|
||||
if (opt != nullptr) {
|
||||
if (! opt->is_vector())
|
||||
ctx->throw_exception("Trying to index a scalar variable", opt_key);
|
||||
ctx->throw_exception(L("Trying to index a scalar variable"), opt_key);
|
||||
char *endptr = nullptr;
|
||||
idx = strtol(opt_key_str.c_str() + idx + 1, &endptr, 10);
|
||||
if (endptr == nullptr || *endptr != 0)
|
||||
ctx->throw_exception("Invalid vector index", IteratorRange(opt_key.begin() + idx + 1, opt_key.end()));
|
||||
ctx->throw_exception(L("Invalid vector index"), IteratorRange(opt_key.begin() + idx + 1, opt_key.end()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opt == nullptr)
|
||||
ctx->throw_exception(ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist", opt_key);
|
||||
ctx->throw_exception(ctx->skipping() ? L("Variable does not exist (in an inactive branch)") : L("Variable does not exist"), opt_key);
|
||||
if (ctx->skipping())
|
||||
return;
|
||||
if (opt->is_scalar()) {
|
||||
if (opt->is_nil())
|
||||
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
|
||||
ctx->throw_exception(L("Trying to reference an undefined (nil) optional variable"), opt_key);
|
||||
output = opt->serialize();
|
||||
} else {
|
||||
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
|
||||
if (vec->empty())
|
||||
ctx->throw_exception("Indexing an empty vector variable", opt_key);
|
||||
ctx->throw_exception(L("Indexing an empty vector variable"), opt_key);
|
||||
if (idx >= vec->size())
|
||||
idx = 0;
|
||||
if (vec->is_nil(idx))
|
||||
ctx->throw_exception("Trying to reference an undefined (nil) element of vector of optional values", opt_key);
|
||||
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt_key);
|
||||
output = vec->vserialize()[idx];
|
||||
}
|
||||
}
|
||||
@@ -1021,7 +1023,7 @@ namespace client
|
||||
if (ctx->skipping() && ! ctx->check_inactive_names())
|
||||
return;
|
||||
|
||||
const char *not_found = ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist";
|
||||
const char *not_found = ctx->skipping() ? L("Variable does not exist (in an inactive branch)") : L("Variable does not exist");
|
||||
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
||||
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
|
||||
if (opt == nullptr) {
|
||||
@@ -1034,24 +1036,24 @@ namespace client
|
||||
ctx->throw_exception(not_found, opt_key);
|
||||
}
|
||||
if (! opt->is_vector())
|
||||
ctx->throw_exception("Trying to index a scalar variable", opt_key);
|
||||
ctx->throw_exception(L("Trying to index a scalar variable"), opt_key);
|
||||
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
|
||||
if (opt_index == nullptr)
|
||||
ctx->throw_exception(not_found, opt_key);
|
||||
if (opt_index->type() != coInt)
|
||||
ctx->throw_exception("Indexing variable has to be integer", opt_key);
|
||||
ctx->throw_exception(L("Indexing variable has to be integer"), opt_key);
|
||||
if (ctx->skipping())
|
||||
return;
|
||||
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
|
||||
if (vec->empty())
|
||||
ctx->throw_exception("Indexing an empty vector variable", opt_key);
|
||||
ctx->throw_exception(L("Indexing an empty vector variable"), opt_key);
|
||||
int idx = opt_index->getInt();
|
||||
if (idx < 0)
|
||||
ctx->throw_exception("Negative vector index", opt_key);
|
||||
ctx->throw_exception(L("Negative vector index"), opt_key);
|
||||
if (idx >= (int)vec->size())
|
||||
idx = 0;
|
||||
if (vec->is_nil(idx))
|
||||
ctx->throw_exception("Trying to reference an undefined (nil) element of vector of optional values", opt_key);
|
||||
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt_key);
|
||||
output = vec->vserialize()[idx];
|
||||
}
|
||||
|
||||
@@ -1066,7 +1068,7 @@ namespace client
|
||||
if (opt == nullptr) {
|
||||
opt = ctx->resolve_output_symbol(key);
|
||||
if (opt == nullptr)
|
||||
ctx->throw_exception("Not a variable name", opt_key);
|
||||
ctx->throw_exception(L("Not a variable name"), opt_key);
|
||||
output.writable = true;
|
||||
}
|
||||
output.opt = opt;
|
||||
@@ -1076,7 +1078,7 @@ namespace client
|
||||
if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr &&
|
||||
ctx->inactive_local_variables.count(key) == 0 &&
|
||||
(ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0))
|
||||
ctx->throw_exception("Not a variable name (in an inactive branch)", opt_key);
|
||||
ctx->throw_exception(L("Not a variable name (in an inactive branch)"), opt_key);
|
||||
}
|
||||
output.it_range = opt_key;
|
||||
}
|
||||
@@ -1110,7 +1112,7 @@ namespace client
|
||||
assert(opt.opt->is_scalar());
|
||||
|
||||
if (opt.opt->is_nil())
|
||||
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt.it_range);
|
||||
ctx->throw_exception(L("Trying to reference an undefined (nil) optional variable"), opt.it_range);
|
||||
|
||||
switch (opt.opt->type()) {
|
||||
case coFloat: output.set_d(opt.opt->getFloat()); break;
|
||||
@@ -1137,7 +1139,7 @@ namespace client
|
||||
for (;;) {
|
||||
const ConfigOption *opt_parent = opt_def->ratio_over.empty() ? nullptr : ctx->resolve_symbol(opt_def->ratio_over);
|
||||
if (opt_parent == nullptr)
|
||||
ctx->throw_exception("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies", opt.it_range);
|
||||
ctx->throw_exception(L("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies"), opt.it_range);
|
||||
if (boost::ends_with(opt_def->ratio_over, "line_width")) {
|
||||
// Line width supports defaults and a complex graph of dependencies.
|
||||
assert(opt_parent->type() == coFloatOrPercent);
|
||||
@@ -1159,7 +1161,7 @@ namespace client
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ctx->throw_exception("Unsupported scalar variable type", opt.it_range);
|
||||
ctx->throw_exception(L("Unsupported scalar variable type"), opt.it_range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1173,7 +1175,7 @@ namespace client
|
||||
assert(opt.opt->is_vector());
|
||||
const ConfigOptionVectorBase* vec = static_cast<const ConfigOptionVectorBase*>(opt.opt);
|
||||
if (vec->empty())
|
||||
ctx->throw_exception("Indexing an empty vector variable", opt.it_range);
|
||||
ctx->throw_exception(L("Indexing an empty vector variable"), opt.it_range);
|
||||
|
||||
// Helper to resolve a FloatOrPercent value (handles ratio_over chain for percent values).
|
||||
// elem_index: the element index used to access this vector element, so that
|
||||
@@ -1194,7 +1196,7 @@ namespace client
|
||||
for (;;) {
|
||||
const ConfigOption *opt_parent = opt_def->ratio_over.empty() ? nullptr : ctx->resolve_symbol(opt_def->ratio_over);
|
||||
if (opt_parent == nullptr)
|
||||
ctx->throw_exception("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies", opt.it_range);
|
||||
ctx->throw_exception(L("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies"), opt.it_range);
|
||||
if (boost::ends_with(opt_def->ratio_over, "line_width")) {
|
||||
// Line width supports defaults and a complex graph of dependencies.
|
||||
assert(opt_parent->type() == coFloatOrPercent);
|
||||
@@ -1277,12 +1279,12 @@ namespace client
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: ctx->throw_exception("Referencing a vector variable when scalar is expected", opt.it_range);
|
||||
default: ctx->throw_exception(L("Referencing a vector variable when scalar is expected"), opt.it_range);
|
||||
}
|
||||
} else {
|
||||
size_t idx = (opt.index < 0) ? 0 : (opt.index >= int(vec->size())) ? 0 : size_t(opt.index);
|
||||
if (vec->is_nil(idx))
|
||||
ctx->throw_exception("Trying to reference an undefined (nil) element of vector of optional values", opt.it_range);
|
||||
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt.it_range);
|
||||
switch (opt.opt->type()) {
|
||||
case coFloats: output.set_d(static_cast<const ConfigOptionFloats*>(opt.opt)->values[idx]); break;
|
||||
case coInts: output.set_i(static_cast<const ConfigOptionInts*>(opt.opt)->values[idx]); break;
|
||||
@@ -1301,27 +1303,27 @@ namespace client
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ctx->throw_exception("Unsupported vector variable type", opt.it_range);
|
||||
ctx->throw_exception(L("Unsupported vector variable type"), opt.it_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void check_writable(const MyContext *ctx, OptWithPos &opt) {
|
||||
if (! opt.writable)
|
||||
ctx->throw_exception("Cannot modify a read-only variable", opt.it_range);
|
||||
ctx->throw_exception(L("Cannot modify a read-only variable"), opt.it_range);
|
||||
}
|
||||
|
||||
static void check_numeric(const expr ¶m) {
|
||||
if (! param.numeric_type())
|
||||
param.throw_exception("Right side is not a numeric expression");
|
||||
param.throw_exception(L("Right side is not a numeric expression"));
|
||||
};
|
||||
|
||||
static size_t evaluate_count(const expr &expr_count) {
|
||||
if (expr_count.type() != expr::TYPE_INT)
|
||||
expr_count.throw_exception("Expected number of elements to fill a vector with.");
|
||||
expr_count.throw_exception(L("Expected number of elements to fill a vector with."));
|
||||
int count = expr_count.i();
|
||||
if (count < 0)
|
||||
expr_count.throw_exception("Negative number of elements specified.");
|
||||
expr_count.throw_exception(L("Negative number of elements specified."));
|
||||
return size_t(count);
|
||||
};
|
||||
|
||||
@@ -1349,11 +1351,11 @@ namespace client
|
||||
break;
|
||||
case coBool:
|
||||
if (rhs.type() != expr::TYPE_BOOL)
|
||||
ctx->throw_exception("Right side is not a boolean expression", rhs.it_range);
|
||||
ctx->throw_exception(L("Right side is not a boolean expression"), rhs.it_range);
|
||||
static_cast<ConfigOptionBool*>(wropt)->value = rhs.b();
|
||||
break;
|
||||
default:
|
||||
ctx->throw_exception("Unsupported output scalar variable type", lhs.it_range);
|
||||
ctx->throw_exception(L("Unsupported output scalar variable type"), lhs.it_range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1363,12 +1365,12 @@ namespace client
|
||||
assert(lhs.opt->is_vector());
|
||||
check_writable(ctx, lhs);
|
||||
if (! lhs.has_index())
|
||||
ctx->throw_exception("Referencing an output vector variable when scalar is expected", lhs.it_range);
|
||||
ctx->throw_exception(L("Referencing an output vector variable when scalar is expected"), lhs.it_range);
|
||||
ConfigOptionVectorBase *vec = const_cast<ConfigOptionVectorBase*>(static_cast<const ConfigOptionVectorBase*>(lhs.opt));
|
||||
if (vec->empty())
|
||||
ctx->throw_exception("Indexing an empty vector variable", lhs.it_range);
|
||||
ctx->throw_exception(L("Indexing an empty vector variable"), lhs.it_range);
|
||||
if (lhs.index >= int(vec->size()))
|
||||
ctx->throw_exception("Index out of range", lhs.it_range);
|
||||
ctx->throw_exception(L("Index out of range"), lhs.it_range);
|
||||
switch (lhs.opt->type()) {
|
||||
case coFloats:
|
||||
check_numeric(rhs);
|
||||
@@ -1387,11 +1389,11 @@ namespace client
|
||||
break;
|
||||
case coBools:
|
||||
if (rhs.type() != expr::TYPE_BOOL)
|
||||
ctx->throw_exception("Right side is not a boolean expression", rhs.it_range);
|
||||
ctx->throw_exception(L("Right side is not a boolean expression"), rhs.it_range);
|
||||
static_cast<ConfigOptionBools*>(vec)->values[lhs.index] = rhs.b();
|
||||
break;
|
||||
default:
|
||||
ctx->throw_exception("Unsupported output vector variable type", lhs.it_range);
|
||||
ctx->throw_exception(L("Unsupported output vector variable type"), lhs.it_range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1414,7 +1416,7 @@ namespace client
|
||||
break;
|
||||
case coBools:
|
||||
if (rhs_value.type() != expr::TYPE_BOOL)
|
||||
rhs_value.throw_exception("Right side is not a boolean expression");
|
||||
rhs_value.throw_exception(L("Right side is not a boolean expression"));
|
||||
static_cast<ConfigOptionBools*>(opt)->values.assign(count, rhs_value.b());
|
||||
break;
|
||||
default: assert(false);
|
||||
@@ -1439,10 +1441,10 @@ namespace client
|
||||
if (ctx->skipping()) {
|
||||
} else if (opt.opt->is_vector()) {
|
||||
if (! opt.has_index())
|
||||
ctx->throw_exception("Referencing a vector variable when scalar is expected", opt.it_range);
|
||||
ctx->throw_exception(L("Referencing a vector variable when scalar is expected"), opt.it_range);
|
||||
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt.opt);
|
||||
if (vec->empty())
|
||||
ctx->throw_exception("Indexing an empty vector variable", opt.it_range);
|
||||
ctx->throw_exception(L("Indexing an empty vector variable"), opt.it_range);
|
||||
output.set_b(static_cast<const ConfigOptionVectorBase*>(opt.opt)->is_nil(opt.index >= int(vec->size()) ? 0 : size_t(opt.index)));
|
||||
} else {
|
||||
assert(opt.opt->is_scalar());
|
||||
@@ -1466,19 +1468,19 @@ namespace client
|
||||
if (! ctx->skipping()) {
|
||||
t_config_option_key key(std::string(it_range.begin(), it_range.end()));
|
||||
if (const ConfigOption* opt = ctx->resolve_symbol(key); opt)
|
||||
ctx->throw_exception("Symbol is already defined in read-only system dictionary", it_range);
|
||||
ctx->throw_exception(L("Symbol is already defined in read-only system dictionary"), it_range);
|
||||
if (ctx->config_outputs && ctx->config_outputs->optptr(key))
|
||||
ctx->throw_exception("Symbol is already defined as system output variable", it_range);
|
||||
ctx->throw_exception(L("Symbol is already defined as system output variable"), it_range);
|
||||
bool has_global_dictionary = ctx->context_data != nullptr && ctx->context_data->global_config;
|
||||
if (global_variable) {
|
||||
if (! has_global_dictionary)
|
||||
ctx->throw_exception("Global variables are not available in this context", it_range);
|
||||
ctx->throw_exception(L("Global variables are not available in this context"), it_range);
|
||||
if (ctx->config_local.optptr(key))
|
||||
ctx->throw_exception("Variable name already defined in local scope", it_range);
|
||||
ctx->throw_exception(L("Variable name already defined in local scope"), it_range);
|
||||
out.opt = ctx->context_data->global_config->optptr(key);
|
||||
} else {
|
||||
if (has_global_dictionary && ctx->context_data->global_config->optptr(key))
|
||||
ctx->throw_exception("Variable name already defined in global scope", it_range);
|
||||
ctx->throw_exception(L("Variable name already defined in global scope"), it_range);
|
||||
out.opt = ctx->config_local.optptr(key);
|
||||
}
|
||||
out.name = std::move(key);
|
||||
@@ -1514,7 +1516,7 @@ namespace client
|
||||
if (ctx->skipping()) {
|
||||
} else if (lhs.opt) {
|
||||
if (lhs.opt->is_vector())
|
||||
rhs.throw_exception("Cannot assign a scalar value to a vector variable.");
|
||||
rhs.throw_exception(L("Cannot assign a scalar value to a vector variable."));
|
||||
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
||||
scalar_variable_assign_scalar(ctx, lhs_opt, rhs);
|
||||
} else {
|
||||
@@ -1540,7 +1542,7 @@ namespace client
|
||||
if (ctx->skipping()) {
|
||||
} else if (lhs.opt) {
|
||||
if (lhs.opt->is_scalar())
|
||||
rhs_value.throw_exception("Cannot assign a vector value to a scalar variable.");
|
||||
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
||||
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
||||
vector_variable_assign_expr_with_count(ctx, lhs_opt, rhs_count, rhs_value);
|
||||
} else {
|
||||
@@ -1566,7 +1568,7 @@ namespace client
|
||||
if (! ctx->skipping()) {
|
||||
check_writable(ctx, lhs);
|
||||
if (lhs.opt->is_scalar())
|
||||
rhs_value.throw_exception("Cannot assign a vector value to a scalar variable.");
|
||||
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
||||
vector_variable_assign_expr_with_count(ctx, lhs, rhs_count, rhs_value);
|
||||
}
|
||||
}
|
||||
@@ -1595,13 +1597,13 @@ namespace client
|
||||
// scalar_var = ()
|
||||
// or
|
||||
// scalar_var = ( scalar, scalar, ... )
|
||||
ctx->throw_exception("Cannot assign a vector value to a scalar variable.", lhs.it_range);
|
||||
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), lhs.it_range);
|
||||
}
|
||||
|
||||
auto check_numeric_vector = [](const std::vector<expr> &il) {
|
||||
for (auto &i : il)
|
||||
if (! i.numeric_type())
|
||||
i.throw_exception("Right side is not a numeric expression");
|
||||
i.throw_exception(L("Right side is not a numeric expression"));
|
||||
};
|
||||
|
||||
ConfigOption *opt = const_cast<ConfigOption*>(lhs.opt);
|
||||
@@ -1620,7 +1622,7 @@ namespace client
|
||||
case coBools:
|
||||
for (auto &i : il)
|
||||
if (i.type() != expr::TYPE_BOOL)
|
||||
i.throw_exception("Right side is not a boolean expression");
|
||||
i.throw_exception(L("Right side is not a boolean expression"));
|
||||
fill_vector_from_initializer_list<ConfigOptionBools>(opt, il, [](auto &v){ return v.b(); });
|
||||
break;
|
||||
default: assert(false);
|
||||
@@ -1642,7 +1644,7 @@ namespace client
|
||||
vector_variable_assign_initializer_list(ctx, lhs_opt, il);
|
||||
} else {
|
||||
if (il.empty())
|
||||
ctx->throw_exception("Cannot create vector variable from an empty initializer list, because its type cannot be deduced.", lhs.it_range);
|
||||
ctx->throw_exception(L("Cannot create vector variable from an empty initializer list, because its type cannot be deduced."), lhs.it_range);
|
||||
// Allocate a new vector variable.
|
||||
// First guesstimate type of the output vector.
|
||||
size_t num_bool = 0;
|
||||
@@ -1663,7 +1665,7 @@ namespace client
|
||||
opt_new = std::make_unique<ConfigOptionStrings>();
|
||||
else if (num_bool > 0) {
|
||||
if (num_double + num_int > 0)
|
||||
ctx->throw_exception("Right side is not valid: Mixing numeric and boolean types.", IteratorRange{ il.front().it_range.begin(), il.back().it_range.end() });
|
||||
ctx->throw_exception(L("Right side is not valid: Mixing numeric and boolean types."), IteratorRange{ il.front().it_range.begin(), il.back().it_range.end() });
|
||||
opt_new = std::make_unique<ConfigOptionBools>();
|
||||
} else {
|
||||
// Output is numeric.
|
||||
@@ -1695,22 +1697,22 @@ namespace client
|
||||
check_writable(ctx, lhs);
|
||||
assert(lhs.opt->is_vector());
|
||||
if (rhs.has_index() || ! rhs.opt->is_vector())
|
||||
ctx->throw_exception("Cannot assign scalar to a vector", lhs.it_range);
|
||||
ctx->throw_exception(L("Cannot assign scalar to a vector"), lhs.it_range);
|
||||
if (rhs.opt->is_nil())
|
||||
ctx->throw_exception("Some elements of the right hand side vector variable of optional values are undefined (nil)", rhs.it_range);
|
||||
ctx->throw_exception(L("Some elements of the right hand side vector variable of optional values are undefined (nil)"), rhs.it_range);
|
||||
if (lhs.opt->type() != rhs.opt->type()) {
|
||||
// Vector types are not compatible.
|
||||
switch (lhs.opt->type()) {
|
||||
case coFloats:
|
||||
ctx->throw_exception("Left hand side is a float vector, while the right hand side is not.", lhs.it_range);
|
||||
ctx->throw_exception(L("Left hand side is a float vector, while the right hand side is not."), lhs.it_range);
|
||||
case coInts:
|
||||
ctx->throw_exception("Left hand side is an int vector, while the right hand side is not.", lhs.it_range);
|
||||
ctx->throw_exception(L("Left hand side is an int vector, while the right hand side is not."), lhs.it_range);
|
||||
case coStrings:
|
||||
ctx->throw_exception("Left hand side is a string vector, while the right hand side is not.", lhs.it_range);
|
||||
ctx->throw_exception(L("Left hand side is a string vector, while the right hand side is not."), lhs.it_range);
|
||||
case coBools:
|
||||
ctx->throw_exception("Left hand side is a bool vector, while the right hand side is not.", lhs.it_range);
|
||||
ctx->throw_exception(L("Left hand side is a bool vector, while the right hand side is not."), lhs.it_range);
|
||||
default:
|
||||
ctx->throw_exception("Left hand side / right hand side vectors are not compatible.", lhs.it_range);
|
||||
ctx->throw_exception(L("Left hand side / right hand side vectors are not compatible."), lhs.it_range);
|
||||
}
|
||||
}
|
||||
const_cast<ConfigOption*>(lhs.opt)->set(rhs.opt);
|
||||
@@ -1735,7 +1737,7 @@ namespace client
|
||||
// Stop parsing, let the other rules resolve this case.
|
||||
return false;
|
||||
if (rhs.opt->is_nil())
|
||||
ctx->throw_exception("Some elements of the right hand side vector variable of optional values are undefined (nil)", rhs.it_range);
|
||||
ctx->throw_exception(L("Some elements of the right hand side vector variable of optional values are undefined (nil)"), rhs.it_range);
|
||||
// Clone the vector variable.
|
||||
std::unique_ptr<ConfigOption> opt_new;
|
||||
if (one_of(rhs.opt->type(), { coFloats, coInts, coStrings, coBools }))
|
||||
@@ -1743,7 +1745,7 @@ namespace client
|
||||
else if (rhs.opt->type() == coPercents)
|
||||
opt_new = std::make_unique<ConfigOptionFloats>(static_cast<const ConfigOptionPercents*>(rhs.opt)->values);
|
||||
else
|
||||
ctx->throw_exception("Duplicating this type of vector variable is not supported", rhs.it_range);
|
||||
ctx->throw_exception(L("Duplicating this type of vector variable is not supported"), rhs.it_range);
|
||||
const_cast<MyContext*>(ctx)->store_new_variable(lhs.name, std::move(opt_new), global_variable);
|
||||
}
|
||||
// Continue parsing.
|
||||
@@ -1761,7 +1763,7 @@ namespace client
|
||||
{
|
||||
if (! ctx->skipping()) {
|
||||
if (opt.has_index() || ! opt.opt->is_vector())
|
||||
ctx->throw_exception("parameter of empty() is not a vector variable", opt.it_range);
|
||||
ctx->throw_exception(L("parameter of empty() is not a vector variable"), opt.it_range);
|
||||
out.set_b(static_cast<const ConfigOptionVectorBase*>(opt.opt)->size() == 0);
|
||||
}
|
||||
out.it_range = opt.it_range;
|
||||
@@ -1771,7 +1773,7 @@ namespace client
|
||||
{
|
||||
if (! ctx->skipping()) {
|
||||
if (opt.has_index() || ! opt.opt->is_vector())
|
||||
ctx->throw_exception("parameter of size() is not a vector variable", opt.it_range);
|
||||
ctx->throw_exception(L("parameter of size() is not a vector variable"), opt.it_range);
|
||||
out.set_i(int(static_cast<const ConfigOptionVectorBase*>(opt.opt)->size()));
|
||||
}
|
||||
out.it_range = opt.it_range;
|
||||
@@ -1783,7 +1785,7 @@ namespace client
|
||||
{
|
||||
if (expr_index.type() != expr::TYPE_EMPTY) {
|
||||
if (expr_index.type() != expr::TYPE_INT)
|
||||
expr_index.throw_exception("Non-integer index is not allowed to address a vector variable.");
|
||||
expr_index.throw_exception(L("Non-integer index is not allowed to address a vector variable."));
|
||||
output = expr_index.i();
|
||||
}
|
||||
}
|
||||
@@ -1794,7 +1796,7 @@ namespace client
|
||||
return;
|
||||
|
||||
if (ctx->context_data == nullptr)
|
||||
ctx->throw_exception("Random number generator not available in this context.",
|
||||
ctx->throw_exception(L("Random number generator not available in this context."),
|
||||
IteratorRange(param1.it_range.begin(), param2.it_range.end()));
|
||||
expr::random(param1, param2, ctx->context_data->rng);
|
||||
}
|
||||
@@ -1832,21 +1834,20 @@ namespace client
|
||||
auto error_line = std::string(first, first_pos) + std::string(last, 0, last_pos);
|
||||
// Position of the it_error from the start of its line.
|
||||
auto error_pos = (it_error - it_begin) - first_pos;
|
||||
msg += "Parsing error at line " + std::to_string(line_nr);
|
||||
if (! info.tag.empty() && info.tag.front() == '*') {
|
||||
// The gat contains an explanatory string.
|
||||
msg += ": ";
|
||||
msg += info.tag.substr(1);
|
||||
// TRN Custom G-code error. %1% is the line number, %2% the reason.
|
||||
msg += Slic3r::format(_u8L("Parsing error at line %1%: %2%"), line_nr, Slic3r::I18N::translate(info.tag.substr(1)));
|
||||
} else {
|
||||
auto it = tag_to_error_message.find(info.tag);
|
||||
if (it == tag_to_error_message.end()) {
|
||||
// A generic error report based on the nonterminal or terminal symbol name.
|
||||
msg += ". Expecting tag ";
|
||||
msg += info.tag;
|
||||
// TRN Custom G-code error. %1% is the line number, %2% the name of a grammar rule.
|
||||
msg += Slic3r::format(_u8L("Parsing error at line %1%. Expecting tag %2%"), line_nr, info.tag);
|
||||
} else {
|
||||
// Use the human readable error message.
|
||||
msg += ". ";
|
||||
msg += it->second;
|
||||
// TRN Custom G-code error. %1% is the line number, %2% the reason.
|
||||
msg += Slic3r::format(_u8L("Parsing error at line %1%. %2%"), line_nr, Slic3r::I18N::translate(it->second));
|
||||
}
|
||||
}
|
||||
msg += '\n';
|
||||
@@ -1877,15 +1878,15 @@ namespace client
|
||||
static void init(const expr &x) {
|
||||
if (x.type() != expr::TYPE_EMPTY) {
|
||||
if (!x.numeric_type())
|
||||
x.throw_exception("Interpolation value must be a number.");
|
||||
x.throw_exception(L("Interpolation value must be a number."));
|
||||
}
|
||||
}
|
||||
static void add_pair(const expr &x, const expr &y, InterpolateTableContext &table) {
|
||||
if (x.type() != expr::TYPE_EMPTY) {
|
||||
if (! x.numeric_type())
|
||||
x.throw_exception("X value of a table point must be a number.");
|
||||
x.throw_exception(L("X value of a table point must be a number."));
|
||||
if (! y.numeric_type())
|
||||
y.throw_exception("Y value of a table point must be a number.");
|
||||
y.throw_exception(L("Y value of a table point must be a number."));
|
||||
table.table.push_back({ x.as_d(), x.it_range, y.as_d() });
|
||||
}
|
||||
}
|
||||
@@ -1902,7 +1903,7 @@ namespace client
|
||||
double x1 = table.table[i].x;
|
||||
if (x0 > x1)
|
||||
boost::throw_exception(qi::expectation_failure(
|
||||
table.table[i - 1].it_range_x.begin(), table.table[i].it_range_x.end(), spirit::info("X coordinates of the table must be increasing")));
|
||||
table.table[i - 1].it_range_x.begin(), table.table[i].it_range_x.end(), spirit::info(std::string("*") + L("X coordinates of the table must be increasing"))));
|
||||
if (! evaluated && x >= x0 && x <= x1) {
|
||||
double y0 = table.table[i - 1].y;
|
||||
double y1 = table.table[i].y;
|
||||
@@ -1925,7 +1926,7 @@ namespace client
|
||||
out.set_d(table.table.back().y);
|
||||
else
|
||||
// The value is really outside the table range.
|
||||
expr_x.throw_exception("Interpolation value is outside the table range");
|
||||
expr_x.throw_exception(L("Interpolation value is outside the table range"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1939,30 +1940,30 @@ namespace client
|
||||
|
||||
// Table to translate symbol tag to a human readable error message.
|
||||
std::map<std::string, std::string> MyContext::tag_to_error_message = {
|
||||
{ "eoi", "Unknown syntax error" },
|
||||
{ "start", "Unknown syntax error" },
|
||||
{ "text", "Invalid text." },
|
||||
{ "text_block", "Invalid text block." },
|
||||
{ "macro", "Invalid macro." },
|
||||
{ "repeat", "Unknown syntax error" },
|
||||
{ "if_else_output", "Not an {if}{else}{endif} macro." },
|
||||
{ "switch_output", "Not a {switch} macro." },
|
||||
{ "legacy_variable_expansion", "Expecting a legacy variable expansion format" },
|
||||
{ "identifier", "Expecting an identifier." },
|
||||
{ "conditional_expression", "Expecting a conditional expression." },
|
||||
{ "logical_or_expression", "Expecting a boolean expression." },
|
||||
{ "logical_and_expression", "Expecting a boolean expression." },
|
||||
{ "equality_expression", "Expecting an expression." },
|
||||
{ "bool_expr_eval", "Expecting a boolean expression."},
|
||||
{ "relational_expression", "Expecting an expression." },
|
||||
{ "additive_expression", "Expecting an expression." },
|
||||
{ "multiplicative_expression", "Expecting an expression." },
|
||||
{ "unary_expression", "Expecting an expression." },
|
||||
{ "optional_parameter", "Expecting a closing brace or an optional parameter." },
|
||||
{ "one_of_list", "Expecting a list of string patterns (simple text or rexep)" },
|
||||
{ "variable_reference", "Expecting a variable reference."},
|
||||
{ "variable", "Expecting a variable name."},
|
||||
{ "regular_expression", "Expecting a regular expression."}
|
||||
{ "eoi", L("Unknown syntax error") },
|
||||
{ "start", L("Unknown syntax error") },
|
||||
{ "text", L("Invalid text.") },
|
||||
{ "text_block", L("Invalid text block.") },
|
||||
{ "macro", L("Invalid macro.") },
|
||||
{ "repeat", L("Unknown syntax error") },
|
||||
{ "if_else_output", L("Not an {if}{else}{endif} macro.") },
|
||||
{ "switch_output", L("Not a {switch} macro.") },
|
||||
{ "legacy_variable_expansion", L("Expecting a legacy variable expansion format") },
|
||||
{ "identifier", L("Expecting an identifier.") },
|
||||
{ "conditional_expression", L("Expecting a conditional expression.") },
|
||||
{ "logical_or_expression", L("Expecting a boolean expression.") },
|
||||
{ "logical_and_expression", L("Expecting a boolean expression.") },
|
||||
{ "equality_expression", L("Expecting an expression.") },
|
||||
{ "bool_expr_eval", L("Expecting a boolean expression.")},
|
||||
{ "relational_expression", L("Expecting an expression.") },
|
||||
{ "additive_expression", L("Expecting an expression.") },
|
||||
{ "multiplicative_expression", L("Expecting an expression.") },
|
||||
{ "unary_expression", L("Expecting an expression.") },
|
||||
{ "optional_parameter", L("Expecting a closing brace or an optional parameter.") },
|
||||
{ "one_of_list", L("Expecting a list of string patterns (simple text or regexp)") },
|
||||
{ "variable_reference", L("Expecting a variable reference.")},
|
||||
{ "variable", L("Expecting a variable name.")},
|
||||
{ "regular_expression", L("Expecting a regular expression.")}
|
||||
};
|
||||
|
||||
// For debugging the boost::spirit parsers. Print out the string enclosed in it_range.
|
||||
@@ -2031,7 +2032,7 @@ namespace client
|
||||
first = it;
|
||||
return true;
|
||||
err:
|
||||
MyContext::throw_exception("Invalid utf8 sequence", IteratorRange(first, last));
|
||||
MyContext::throw_exception(L("Invalid utf8 sequence"), IteratorRange(first, last));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2057,7 +2058,7 @@ namespace client
|
||||
return false;
|
||||
char c = *first;
|
||||
if (it - first > 1 || c < 0)
|
||||
MyContext::throw_exception("Non-ASCII7 characters are only allowed inside text blocks and string literals, not inside code blocks.", IteratorRange(first, it));
|
||||
MyContext::throw_exception(L("Non-ASCII7 characters are only allowed inside text blocks and string literals, not inside code blocks."), IteratorRange(first, it));
|
||||
if (c == '\r' || c == '\n' || c == '\t' || c == ' ') {
|
||||
// Skip the whitespaces
|
||||
++ first;
|
||||
@@ -2116,7 +2117,7 @@ namespace client
|
||||
if (*it == '\\') {
|
||||
if (++ it == end ||
|
||||
(*it != 'r' && *it != 'n' && *it != '"' && *it != '\\'))
|
||||
ctx->throw_exception("Invalid escape sequence", {std::prev(it), std::next(it) });
|
||||
ctx->throw_exception(L("Invalid escape sequence"), {std::prev(it), std::next(it) });
|
||||
++ len;
|
||||
++ it;
|
||||
} else {
|
||||
|
||||
+116
-2
@@ -36,6 +36,7 @@
|
||||
#include "Semver.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
#include "I18N.hpp"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -81,6 +82,7 @@
|
||||
#include "LifecycleEvents.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "InstanceLock.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include "PlaceholderParser.hpp"
|
||||
@@ -2757,6 +2759,118 @@ void PresetCollection::update_after_user_presets_loaded()
|
||||
return;
|
||||
}
|
||||
|
||||
// A user preset file as written: it holds only the keys the preset sets itself, and an inherits that names a
|
||||
// renamed parent by the old name the preset was made against. Throws if the file can not be read or parsed.
|
||||
static json read_user_preset_file(const std::string &file)
|
||||
{
|
||||
boost::nowide::ifstream in(file);
|
||||
return json::parse(in);
|
||||
}
|
||||
|
||||
std::vector<LegacyUserFilamentPreset> PresetCollection::legacy_user_filament_presets(const std::map<std::string, std::vector<std::string>> &printers_by_parent)
|
||||
{
|
||||
auto has_entry = [&printers_by_parent](const std::string &name) { return printers_by_parent.count(name) > 0; };
|
||||
std::vector<LegacyUserFilamentPreset> legacy_presets;
|
||||
// The preset sync thread can add or delete presets meanwhile.
|
||||
lock();
|
||||
for (const Preset &preset : m_presets) {
|
||||
if (!preset.is_user() || preset.inherits().empty())
|
||||
continue;
|
||||
const Preset *parent = get_preset_parent(preset);
|
||||
if (parent == nullptr || !parent->is_system)
|
||||
continue;
|
||||
// An empty list already covers every printer.
|
||||
const std::vector<std::string> &own = preset.config.option<ConfigOptionStrings>("compatible_printers")->values;
|
||||
if (own.empty())
|
||||
continue;
|
||||
// The parent's name in the older release is its current name or one in its renamed_from, so only the
|
||||
// files of presets whose parent has an entry under one of those are read.
|
||||
if (!has_entry(parent->name) && std::none_of(parent->renamed_from.begin(), parent->renamed_from.end(), has_entry))
|
||||
continue;
|
||||
LegacyUserFilamentPreset legacy{preset.name, parent->name, {}, {}, preset.file, own};
|
||||
try {
|
||||
const json j = read_user_preset_file(preset.file);
|
||||
// A preset whose file sets compatible_printers, even to an empty list, was given its printers on
|
||||
// purpose and keeps exactly those.
|
||||
if (j.contains("compatible_printers"))
|
||||
continue;
|
||||
legacy.inherits = j.value(BBL_JSON_KEY_INHERITS, std::string());
|
||||
} catch (const std::exception &err) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": can not read " << preset.file << ": " << err.what();
|
||||
continue;
|
||||
}
|
||||
auto it = printers_by_parent.find(legacy.inherits);
|
||||
if (it == printers_by_parent.end())
|
||||
continue;
|
||||
for (const std::string &printer : it->second)
|
||||
if (std::find(own.begin(), own.end(), printer) == own.end()) {
|
||||
legacy.printers.push_back(printer);
|
||||
legacy.compatible_printers.push_back(printer);
|
||||
}
|
||||
if (!legacy.printers.empty())
|
||||
legacy_presets.push_back(std::move(legacy));
|
||||
}
|
||||
unlock();
|
||||
return legacy_presets;
|
||||
}
|
||||
|
||||
bool PresetCollection::write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy)
|
||||
{
|
||||
// Only compatible_printers changes in the file. Saving the whole preset would also rewrite an inherits that
|
||||
// names a renamed parent by its old name, the only name older releases know it by.
|
||||
try {
|
||||
json j = read_user_preset_file(legacy.file);
|
||||
// Changed by hand or by another instance since the preset was found: a list of its own stands, and
|
||||
// another parent would need other printers.
|
||||
if (j.contains("compatible_printers") || j.value(BBL_JSON_KEY_INHERITS, std::string()) != legacy.inherits)
|
||||
return false;
|
||||
j["compatible_printers"] = legacy.compatible_printers;
|
||||
if (const std::error_code ec = write_file_atomically(legacy.file, j.dump(1, '\t') + "\n")) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << legacy.file << ": " << ec.message();
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception &err) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not migrate " << legacy.file << ": " << err.what();
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": user preset " << legacy.preset << " gets " << legacy.printers.size()
|
||||
<< " printer(s) its parent listed in an older release";
|
||||
return true;
|
||||
}
|
||||
|
||||
void PresetCollection::apply_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &written)
|
||||
{
|
||||
lock();
|
||||
for (const LegacyUserFilamentPreset &legacy : written) {
|
||||
auto it = this->find_preset_internal(legacy.preset);
|
||||
if (it == m_presets.end() || it->name != legacy.preset)
|
||||
continue; // deleted by the preset sync thread meanwhile
|
||||
std::vector<std::string> &saved = it->config.option<ConfigOptionStrings>("compatible_printers", true)->values;
|
||||
std::vector<std::string> &edited = m_edited_preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values;
|
||||
// Added to the list the preset holds now rather than replacing it: the cloud sync may have given the
|
||||
// preset a newer list since it was found, and that list is still to be written out.
|
||||
std::vector<std::string> printers = saved;
|
||||
for (const std::string &printer : legacy.printers)
|
||||
if (std::find(printers.begin(), printers.end(), printer) == printers.end())
|
||||
printers.push_back(printer);
|
||||
// A change to the list pending in the settings tab stays pending. Without one, the edited copy and the
|
||||
// snapshot unsaved changes are measured against take the written list, so nothing reads as modified.
|
||||
if (m_idx_selected == static_cast<size_t>(it - m_presets.begin()) && edited == saved) {
|
||||
edited = printers;
|
||||
if (m_saved_preset.name == it->name)
|
||||
m_saved_preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values = printers;
|
||||
}
|
||||
saved = printers;
|
||||
// Push the change to the cloud copy, as an edit in the settings tab would. A sync already pending for
|
||||
// the preset (a download waiting to be saved, a deletion) keeps its state.
|
||||
if (!it->setting_id.empty() && it->sync_info.empty()) {
|
||||
it->sync_info = "update";
|
||||
it->save_info();
|
||||
}
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
//BBS: validate_preset
|
||||
bool PresetCollection::validate_preset(const std::string &preset_name, std::string &inherit_name)
|
||||
{
|
||||
@@ -4640,10 +4754,10 @@ void PhysicalPrinterCollection::load_printers(
|
||||
printer.loaded = true;
|
||||
}
|
||||
catch (const std::ifstream::failure& err) {
|
||||
throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + printer.file + "\n\tReason: " + err.what());
|
||||
throw Slic3r::RuntimeError((boost::format(_u8L("The selected preset cannot be loaded: %1%\n\tReason: %2%")) % printer.file % err.what()).str());
|
||||
}
|
||||
catch (const std::runtime_error& err) {
|
||||
throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + printer.file + "\n\tReason: " + err.what());
|
||||
throw Slic3r::RuntimeError((boost::format(_u8L("Failed loading the preset file: %1%\n\tReason: %2%")) % printer.file % err.what()).str());
|
||||
}
|
||||
printers_loaded.emplace_back(printer);
|
||||
}
|
||||
|
||||
@@ -551,6 +551,20 @@ struct PresetConfigSubstitutions {
|
||||
// PrusaSlicer and reading the user Print / Filament / Printer profiles.
|
||||
using PresetsConfigSubstitutions = std::vector<PresetConfigSubstitutions>;
|
||||
|
||||
// A user filament preset made against an older release that no longer shows on printers its system parent
|
||||
// listed then, and those printers. Migrating it writes them into the preset's own compatible_printers.
|
||||
struct LegacyUserFilamentPreset {
|
||||
std::string preset;
|
||||
std::string parent;
|
||||
// The parent's name as the preset file states it: the name the older release knew it by.
|
||||
std::string inherits;
|
||||
// The printers the preset lost: what the prompt lists.
|
||||
std::vector<std::string> printers;
|
||||
std::string file;
|
||||
// The list the file gets: the parent's current printers plus the lost ones.
|
||||
std::vector<std::string> compatible_printers;
|
||||
};
|
||||
|
||||
// Collections of presets of the same type (one of the Print, Filament or Printer type).
|
||||
class PresetCollection
|
||||
{
|
||||
@@ -601,6 +615,18 @@ public:
|
||||
void save_user_presets(const std::string& dir_path, const std::string& type, std::map<std::string, std::string>& need_to_delete_list);
|
||||
bool load_user_preset(std::string name, std::map<std::string, std::string> preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, const PresetOrigin &load_origin = PresetOrigin(PresetOrigin::Kind::User));
|
||||
void update_after_user_presets_loaded();
|
||||
// User presets store only their diff, so a user preset inherits its system parent's compatible_printers
|
||||
// and disappears from a printer once a profile update narrows that list. Lists the user presets whose
|
||||
// files do not set compatible_printers, and the printers their parent covered in an older release but
|
||||
// no longer lists. printers_by_parent is keyed by the parent's name in that release, which is the name
|
||||
// the preset file's inherits still holds.
|
||||
std::vector<LegacyUserFilamentPreset> legacy_user_filament_presets(const std::map<std::string, std::vector<std::string>> &printers_by_parent);
|
||||
// Writes legacy.compatible_printers into the preset's file and keeps every other key. The caller holds the
|
||||
// user presets lock. False if the file has gained a compatible_printers key or another parent since the
|
||||
// preset was found, or could not be written.
|
||||
static bool write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy);
|
||||
// Gives the loaded presets the lists their files were just given, and marks cloud copies for upload.
|
||||
void apply_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &written);
|
||||
//BBS: get user presets
|
||||
int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &result_presets);
|
||||
void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id);
|
||||
|
||||
+131
-40
@@ -2,6 +2,7 @@
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/directory.hpp>
|
||||
#include <boost/filesystem/file_status.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
@@ -38,6 +39,8 @@
|
||||
#include "ParallelResolve.hpp"
|
||||
#include "Preset.hpp"
|
||||
#include "PresetCacheFormat.hpp"
|
||||
#include "Time.hpp"
|
||||
#include "Zipper.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "PublishSettings.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
@@ -96,6 +99,21 @@ using json = nlohmann::json;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Per system filament preset of an older release, by its name then, the printers it listed that the preset it
|
||||
// now loads as does not (resources/info/legacy_compatible_printers.json, written by orca_profile_tool.py
|
||||
// legacy-compat). Empty if the file is missing or malformed.
|
||||
static std::map<std::string, std::vector<std::string>> legacy_compatible_printers()
|
||||
{
|
||||
const fs::path path = fs::path(resources_dir()) / "info" / "legacy_compatible_printers.json";
|
||||
try {
|
||||
boost::nowide::ifstream in(path.string());
|
||||
return json::parse(in).at("presets").get<std::map<std::string, std::vector<std::string>>>();
|
||||
} catch (const std::exception &err) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not read " << path.string() << ": " << err.what();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Project-level options imported from a loaded 3MF into project_config. s_project_options_published
|
||||
// below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync.
|
||||
// s_project_options_published additionally carries wipe_tower_rotation_angle, which normal
|
||||
@@ -809,7 +827,7 @@ void PresetBundle::setup_directories()
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("creating directory %1% failed, maybe created by other instance, go on!")%subdir.string();
|
||||
}
|
||||
else
|
||||
throw Slic3r::RuntimeError(std::string("Unable to create directory ") + subdir.string());
|
||||
throw Slic3r::RuntimeError((boost::format(L("Unable to create directory %1%")) % subdir.string()).str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1045,6 +1063,73 @@ bool PresetBundle::backup_user_folder() const
|
||||
}
|
||||
}
|
||||
|
||||
bool PresetBundle::backup_user_folder_to_zip(const std::string &zip_path) const
|
||||
{
|
||||
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
|
||||
BOOST_LOG_TRIVIAL(info) << "Backing up user folder to: " << zip_path;
|
||||
try {
|
||||
Zipper zipper(zip_path);
|
||||
// Directories get their own entries, which extract_archive_confined() needs to recreate them.
|
||||
zipper.add_entry(std::string(PRESET_USER_DIR) + "/", nullptr, 0);
|
||||
// The loader follows symlinked folders, and migration writes the presets in them.
|
||||
for (const fs::directory_entry &entry : fs::recursive_directory_iterator(user_dir, fs::directory_options::follow_directory_symlink)) {
|
||||
const std::string name = (fs::path(PRESET_USER_DIR) / entry.path().lexically_relative(user_dir)).generic_string();
|
||||
if (fs::is_directory(entry.status())) {
|
||||
zipper.add_entry(name + "/", nullptr, 0);
|
||||
continue;
|
||||
}
|
||||
if (!fs::is_regular_file(entry.status()))
|
||||
continue;
|
||||
boost::nowide::ifstream in(entry.path().string(), std::ios::binary);
|
||||
const std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
if (!in.good() && !in.eof())
|
||||
throw Slic3r::RuntimeError("can not read " + entry.path().string());
|
||||
zipper.add_entry(name, data.data(), data.size());
|
||||
}
|
||||
zipper.finalize();
|
||||
return true;
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Exception during user folder backup: " << ex.what();
|
||||
boost::system::error_code ec;
|
||||
fs::remove(zip_path, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<LegacyUserFilamentPreset> PresetBundle::legacy_user_filament_presets()
|
||||
{
|
||||
return filaments.legacy_user_filament_presets(legacy_compatible_printers());
|
||||
}
|
||||
|
||||
PresetBundle::LegacyUserFilamentPresetMigration PresetBundle::migrate_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &legacy_presets)
|
||||
{
|
||||
LegacyUserFilamentPresetMigration result;
|
||||
std::vector<LegacyUserFilamentPreset> written;
|
||||
const std::string zip_path = data_dir() + "/filament_preset_migration_" +
|
||||
Utils::time2str(Utils::TimeZone::utc, Utils::TimeFormat::iso8601Z) + "_backup.zip";
|
||||
// Before the lock: zipping a large folder takes longer than a guard may be held.
|
||||
if (!backup_user_folder_to_zip(zip_path))
|
||||
return result;
|
||||
result.backup = zip_path;
|
||||
{
|
||||
// One hold for every write, so a lock another instance keeps is one outcome rather than a failure per
|
||||
// preset (the lock backs off for a while after a miss). Held without the collection mutex, the order
|
||||
// InstanceLock requires; each write re-reads its file, so a save that slipped in before is respected.
|
||||
const std::string lock_path = user_presets_lock_path();
|
||||
InstanceLock instance_lock(lock_path);
|
||||
if (!lock_path.empty() && !instance_lock.locked())
|
||||
return result;
|
||||
result.locked = true;
|
||||
for (const LegacyUserFilamentPreset &legacy : legacy_presets)
|
||||
if (PresetCollection::write_legacy_user_filament_preset(legacy))
|
||||
written.push_back(legacy);
|
||||
else
|
||||
result.failed.push_back(legacy.preset);
|
||||
}
|
||||
filaments.apply_legacy_user_filament_presets(written);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const
|
||||
{
|
||||
if (filament_id.empty())
|
||||
@@ -5451,7 +5536,7 @@ ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, Forw
|
||||
|
||||
//BBS: add config related logs
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode")%path ;
|
||||
throw Slic3r::RuntimeError(std::string("Unknown configuration file: ") + path);
|
||||
throw Slic3r::RuntimeError((boost::format(L("Unknown configuration file: %1%")) % path).str());
|
||||
|
||||
return ConfigSubstitutions{};
|
||||
}
|
||||
@@ -5653,7 +5738,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
ConfigOptionStrings* filament_colour_option = config.option<ConfigOptionStrings>("filament_colour");
|
||||
size_t num_filaments = filament_colour_option?filament_colour_option->size():0;
|
||||
if (num_filaments == 0)
|
||||
throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path);
|
||||
throw Slic3r::RuntimeError((boost::format(L("Invalid configuration file: %1%")) % name_or_path).str());
|
||||
#endif
|
||||
|
||||
//BBS: add config related logs
|
||||
@@ -5717,7 +5802,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
|| (extruder_variant_count < num_filaments)) {
|
||||
assert(false);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path;
|
||||
throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path);
|
||||
throw Slic3r::RuntimeError((boost::format(L("Invalid configuration file: %1%")) % name_or_path).str());
|
||||
}
|
||||
if (num_filaments != extruder_variant_count) {
|
||||
process_multi_extruder = true;
|
||||
@@ -6151,8 +6236,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
// A payload mix authored at the same slot (a mix inside the physical region) is
|
||||
// processed after this sweep and overrides its own destination below.
|
||||
published_config->mixed_slot_relocations.insert_or_assign(int(slot), int(dest));
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + " -> slot " +
|
||||
std::to_string(dest) + ": mixed filament");
|
||||
// TRN Filament slot change after loading a published project. %1% and %2% are slot numbers.
|
||||
published_config->material_replacements.emplace_back(
|
||||
(boost::format(L("slot %1% -> slot %2%: mixed filament")) % slot % dest).str());
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated receiver mixed filament slot " << slot
|
||||
<< " -> " << dest << " (physical-first rebalance)";
|
||||
}
|
||||
@@ -6186,9 +6272,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
// No free virtual slot left: report instead of destroying a real filament.
|
||||
// The local skipped_keys is published wholesale at the end of the pass;
|
||||
// writing published_config->skipped_keys here would be clobbered by it.
|
||||
skipped_keys.emplace_back("material:" + material_label + (is_payload_mix ?
|
||||
" (mixed filament definition: filament slot limit reached)" :
|
||||
" (filament slot limit reached)"));
|
||||
skipped_keys.emplace_back("material:" + material_label + " (" +
|
||||
(is_payload_mix ? L("mixed filament definition: filament slot limit reached") :
|
||||
L("filament slot limit reached")) +
|
||||
")");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF " << (is_payload_mix ? "mixed filament definition" : "material")
|
||||
<< " from slot " << entry.slot << " could not be placed: all " << next_free_slot
|
||||
<< " slots exhausted";
|
||||
@@ -6200,8 +6287,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
// sidebar's mixed section needs two physical filaments to mix), so a
|
||||
// placeholder would be invisible and unfixable: drop the entry and
|
||||
// report it like any other unappliable input.
|
||||
skipped_keys.emplace_back("material:" + material_label + " (printer supports only " +
|
||||
std::to_string(physical_capacity) + " filament)");
|
||||
skipped_keys.emplace_back("material:" + material_label + " (" +
|
||||
(boost::format(L("the printer's filament limit is %1%")) % physical_capacity).str() + ")");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF material from slot " << entry.slot
|
||||
<< " dropped: printer supports only " << physical_capacity << " filament";
|
||||
entry_it = published_config->material_keys.erase(entry_it);
|
||||
@@ -6219,8 +6306,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
any_mixed_relocated = true;
|
||||
mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot));
|
||||
published_config->mixed_slot_relocations.insert_or_assign(authored_slot, entry.slot);
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " +
|
||||
std::to_string(entry.slot) + ": mixed filament");
|
||||
published_config->material_replacements.emplace_back(
|
||||
(boost::format(L("slot %1% -> slot %2%: mixed filament")) % authored_slot % entry.slot).str());
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot
|
||||
<< " -> " << entry.slot;
|
||||
++entry_it;
|
||||
@@ -6238,10 +6325,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
published_config->mixed_slot_relocations.insert_or_assign(authored_slot, dest_slot);
|
||||
}
|
||||
published_config->material_replacements.emplace_back(
|
||||
(dest_slot != authored_slot ?
|
||||
"slot " + std::to_string(authored_slot) + " -> slot " + std::to_string(dest_slot) :
|
||||
"slot " + std::to_string(dest_slot)) +
|
||||
": unassigned mixed filament (printer supports only " + std::to_string(physical_capacity) + " filaments)");
|
||||
dest_slot != authored_slot ?
|
||||
(boost::format(L("slot %1% -> slot %2%: unassigned mixed filament (the printer's filament limit is %3%)")) %
|
||||
authored_slot % dest_slot % physical_capacity).str() :
|
||||
(boost::format(L("slot %1%: unassigned mixed filament (the printer's filament limit is %2%)")) % dest_slot %
|
||||
physical_capacity).str());
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF material from slot " << authored_slot
|
||||
<< " placed as an unassigned mixed filament at slot " << dest_slot
|
||||
<< " (printer supports only " << physical_capacity << " filaments)";
|
||||
@@ -6463,8 +6551,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
this->filament_presets[slot] = replacement;
|
||||
material_applied = true;
|
||||
// The re-point used to be silent; surface it like the other slot changes.
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + ": " + aliased_name + " -> " +
|
||||
replacement);
|
||||
// TRN Filament slot change after loading a published project. %1% is the slot number, %2% the old preset, %3% the new one.
|
||||
published_config->material_replacements.emplace_back(
|
||||
(boost::format(L("slot %1%: %2% -> %3%")) % slot % aliased_name % replacement).str());
|
||||
}
|
||||
// Grow the per-slot colour/type/map project vectors to the new slot count and
|
||||
// seed the new entries so the slots render with colours instead of blank chips
|
||||
@@ -6659,8 +6748,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
// definition below.
|
||||
if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) {
|
||||
finalize_mixed_placeholder(slot);
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
|
||||
": mixed filament definition could not be imported");
|
||||
published_config->material_replacements.emplace_back(
|
||||
(boost::format(L("slot %1%: mixed filament definition could not be imported")) % slot).str());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -6687,17 +6776,18 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
if (comps.size() < 2)
|
||||
// An empty definition cell counts as broken too: applying it
|
||||
// would ship a mix the sidebar would only flag later.
|
||||
mix_error = "needs at least two components";
|
||||
mix_error = L("needs at least two components");
|
||||
else
|
||||
for (unsigned int comp : comps)
|
||||
if (comp < 1 || size_t(comp) > mixed_final_slot_count ||
|
||||
mixed_final_slots.count(int(comp) - 1) != 0) {
|
||||
mix_error = "components reference missing slots";
|
||||
mix_error = L("components reference missing slots");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mix_error.empty()) {
|
||||
skipped_keys.emplace_back("material:" + material_label + " (mixed filament definition: " + mix_error + ")");
|
||||
skipped_keys.emplace_back("material:" + material_label + " (" +
|
||||
(boost::format(L("mixed filament definition: %1%")) % mix_error).str() + ")");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot
|
||||
<< " rejected: " << mix_error;
|
||||
// Degradation, not silent corruption: the slot was already grown and
|
||||
@@ -6713,9 +6803,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
// is reported as skipped above.
|
||||
if (!this->is_mixed_filament(slot)) {
|
||||
finalize_mixed_placeholder(slot);
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
|
||||
": mixed filament definition could not be imported (" +
|
||||
mix_error + ")");
|
||||
published_config->material_replacements.emplace_back(
|
||||
(boost::format(L("slot %1%: mixed filament definition could not be imported (%2%)")) % slot % mix_error).str());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -6786,8 +6875,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
const std::string old_name = this->filament_presets[slot];
|
||||
this->filament_presets[slot] = new_name;
|
||||
material_applied = true;
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + ": " + old_name + " -> " +
|
||||
new_name);
|
||||
published_config->material_replacements.emplace_back(
|
||||
(boost::format(L("slot %1%: %2% -> %3%")) % slot % old_name % new_name).str());
|
||||
// Colour is slot-scoped and project-visible: sync into project_config
|
||||
// (the copy already baked it, this makes the chips render).
|
||||
if (entry.publish_color && !entry.color.empty()) {
|
||||
@@ -6906,11 +6995,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
<< " (score " << score << ", preset_name \"" << entry.preset_name << "\", filament_id \""
|
||||
<< entry.filament_id << "\", setting_id \"" << entry.setting_id << "\", type \""
|
||||
<< entry.publish_type_value << "\")";
|
||||
std::string replacement_line = "slot " + std::to_string(slot) + ": " + old_name + " -> " + replacement;
|
||||
// A pick that is not the exact published material is a substitute;
|
||||
// an entry without identity fields cannot be judged, so it stays plain.
|
||||
if (score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty()))
|
||||
replacement_line += " (substitute)";
|
||||
const bool is_substitute = score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty());
|
||||
std::string replacement_line = (boost::format(is_substitute ? L("slot %1%: %2% -> %3% (substitute)") :
|
||||
L("slot %1%: %2% -> %3%")) %
|
||||
slot % old_name % replacement).str();
|
||||
published_config->material_replacements.emplace_back(std::move(replacement_line));
|
||||
} else {
|
||||
// Partial publish with no replacement: keep the receiver's
|
||||
@@ -6926,8 +7016,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
// mix is left untouched).
|
||||
if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) {
|
||||
finalize_mixed_placeholder(slot);
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
|
||||
": mixed filament definition could not be imported");
|
||||
published_config->material_replacements.emplace_back(
|
||||
(boost::format(L("slot %1%: mixed filament definition could not be imported")) % slot).str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7010,8 +7100,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
if (!this->is_mixed_filament(size_t(gap_slot))) {
|
||||
finalize_mixed_placeholder(size_t(gap_slot));
|
||||
published_config->material_replacements.emplace_back(
|
||||
"slot " + std::to_string(gap_slot) +
|
||||
": unassigned mixed filament (printer supports only " + std::to_string(physical_capacity) + " filaments)");
|
||||
(boost::format(L("slot %1%: unassigned mixed filament (the printer's filament limit is %2%)")) % gap_slot %
|
||||
physical_capacity).str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7064,7 +7154,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
|
||||
static ConfigurationError failed_loading_error(const std::string& file, const std::string& dir)
|
||||
{
|
||||
return ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % file % dir).str());
|
||||
return ConfigurationError((boost::format(L("Failed loading configuration file %1%\nSuggest cleaning the directory %2% first")) % file % dir).str());
|
||||
}
|
||||
|
||||
void PresetBundle::log_errors(const std::vector<std::string>& errors)
|
||||
@@ -7537,7 +7627,8 @@ void PresetBundle::parse_vendor_json(VendorRead& read)
|
||||
auto config_version = Semver::parse(version_str);
|
||||
if (! config_version) {
|
||||
++m_errors;
|
||||
throw ConfigurationError((boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly")
|
||||
// TRN %1% is the vendor name, %2% the version found in its profile, %3% the profile directory.
|
||||
throw ConfigurationError((boost::format(L("Vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% first"))
|
||||
% vendor_name % version_str % dir).str());
|
||||
} else {
|
||||
vendor_profile.config_version = std::move(*config_version);
|
||||
@@ -7575,7 +7666,7 @@ void PresetBundle::parse_vendor_json(VendorRead& read)
|
||||
}
|
||||
catch(nlohmann::detail::parse_error &err) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<<root_file<<" got a nlohmann::detail::parse_error, reason = " << err.what();
|
||||
throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly")
|
||||
throw ConfigurationError((boost::format(L("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% first"))
|
||||
%root_file %err.what() % dir).str());
|
||||
//goto __error_process;
|
||||
}
|
||||
@@ -7690,7 +7781,7 @@ void PresetBundle::parse_vendor_json(VendorRead& read)
|
||||
}
|
||||
catch(nlohmann::detail::parse_error &err) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what();
|
||||
throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly")
|
||||
throw ConfigurationError((boost::format(L("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% first"))
|
||||
%subfile %err.what() % dir).str());
|
||||
}
|
||||
|
||||
|
||||
@@ -376,6 +376,21 @@ public:
|
||||
bool use_bbl_device_tab();
|
||||
|
||||
bool backup_user_folder() const;
|
||||
// Writes the user preset folder into a zip archive at zip_path, its entries under "user/".
|
||||
bool backup_user_folder_to_zip(const std::string &zip_path) const;
|
||||
|
||||
// User filament presets made against an older release that no longer show on printers their parent
|
||||
// listed then (resources/info/legacy_compatible_printers.json).
|
||||
std::vector<LegacyUserFilamentPreset> legacy_user_filament_presets();
|
||||
// Backs up the user preset folder into a zip in data_dir(), then writes each preset's printers into its file
|
||||
// under one hold of the user presets lock. Nothing is written when the backup failed (backup empty) or the
|
||||
// lock could not be taken (locked false); otherwise failed names the presets whose files could not be written.
|
||||
struct LegacyUserFilamentPresetMigration {
|
||||
bool locked{false};
|
||||
std::string backup;
|
||||
std::vector<std::string> failed;
|
||||
};
|
||||
LegacyUserFilamentPresetMigration migrate_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &legacy_presets);
|
||||
|
||||
//BBS: project embedded preset logic
|
||||
PresetsConfigSubstitutions load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule);
|
||||
|
||||
+26
-20
@@ -1484,7 +1484,7 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
// return {inst->model_instance->get_object()->name + L(" is too close to exclusion area, there may be collisions when printing.") + "\n",
|
||||
// inst->model_instance->get_object()};
|
||||
//ORCA: Pass ModelInstance instead of ModelObject
|
||||
return {inst->model_instance->get_object()->name + L(" is too close to exclusion area, there may be collisions when printing.") + "\n",
|
||||
return {(boost::format(L("%1% is too close to exclusion area. There may be collisions when printing.")) % inst->model_instance->get_object()->name).str() + "\n",
|
||||
inst->model_instance};
|
||||
}
|
||||
|
||||
@@ -1492,7 +1492,7 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
// return {inst->model_instance->get_object()->name + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n",
|
||||
// inst->model_instance->get_object()};
|
||||
//ORCA: Pass ModelInstance instead of ModelObject
|
||||
return {inst->model_instance->get_object()->name + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n",
|
||||
return {(boost::format(L("%1% is too close to the clumping detection area. There may be collisions when printing.")) % inst->model_instance->get_object()->name).str() + "\n",
|
||||
inst->model_instance};
|
||||
}
|
||||
current_instance_hulls.emplace_back(volume_hull);
|
||||
@@ -1577,20 +1577,20 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
// many setups that print fine.
|
||||
if (!intersection(convex_hulls_other, convex_hulls_temp).empty()) {
|
||||
if (warning) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n");
|
||||
warning->string += L("Prime tower is too close to others, and collisions may be caused.\n");
|
||||
}
|
||||
}
|
||||
if (!intersection(exclude_polys, tower_polys_checked).empty()) {
|
||||
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")};
|
||||
return {L("Prime tower is too close to an exclusion area, and collisions will be caused.\n")};
|
||||
}
|
||||
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_checked).empty()) {
|
||||
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
|
||||
return {L("Prime tower is too close to the clumping detection area, and collisions will be caused.\n")};
|
||||
}
|
||||
if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to exclusion area, there may be collisions when printing.") + "\n";
|
||||
warning->string += L("Prime tower is too close to an exclusion area. There may be collisions when printing.") + "\n";
|
||||
}
|
||||
if (warning && print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_estimated).empty()) {
|
||||
warning->string += L("Prime Tower") + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n";
|
||||
warning->string += L("Prime tower is too close to the clumping detection area. There may be collisions when printing.") + "\n";
|
||||
}
|
||||
// No gate on "is there a tower": one that is not printed estimates to zero, so the hulls
|
||||
// are degenerate and every check passes. Re-deriving it here missed the wrapping-detection
|
||||
@@ -1600,9 +1600,9 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
for (Polygon &p : printable_polys)
|
||||
p.translate(plate_shift);
|
||||
if (!diff(tower_polys_checked, printable_polys).empty())
|
||||
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
|
||||
return {L("Prime tower is partially outside the printable area, and it cannot be printed.\n")};
|
||||
if (warning && !diff(tower_polys_estimated, printable_polys).empty())
|
||||
warning->string += L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n");
|
||||
warning->string += L("Prime tower is partially outside the printable area, and it cannot be printed.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1969,14 +1969,19 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
// What is left is a printer with more filaments than logical extruders, where
|
||||
// the tail slots fall outside the map. Narrow, but without this the sentence
|
||||
// ends in a dangling "on .".
|
||||
if (routed_list.empty())
|
||||
routed_list = L("no configured tool");
|
||||
|
||||
StringObjectException err;
|
||||
err.string = Slic3r::format(
|
||||
L("IDEX/IQEX mode \"%1%\" prints with T%2%, but this plate's filaments are on %3%. "
|
||||
"Assign a filament loaded on T%2%, or switch this plate to Primary mode."),
|
||||
parallel_mode, routing.primary_phys, routed_list);
|
||||
if (routed_list.empty())
|
||||
err.string = Slic3r::format(
|
||||
// TRN %1% is the IDEX/IQEX mode name, %2% the tool number.
|
||||
L("IDEX/IQEX mode \"%1%\" prints with T%2%, but none of this plate's filaments are on a configured tool. "
|
||||
"Assign a filament loaded on T%2%, or switch this plate to Primary mode."),
|
||||
parallel_mode, routing.primary_phys);
|
||||
else
|
||||
err.string = Slic3r::format(
|
||||
// TRN %1% is the IDEX/IQEX mode name, %2% the tool number, %3% a list of tools such as "T1, T2".
|
||||
L("IDEX/IQEX mode \"%1%\" prints with T%2%, but this plate's filaments are on %3%. "
|
||||
"Assign a filament loaded on T%2%, or switch this plate to Primary mode."),
|
||||
parallel_mode, routing.primary_phys, routed_list);
|
||||
// Gives the notification a "Jump to <object>" link, which selects the object
|
||||
// and switches to Prepare -- directly enabling the first suggested remedy.
|
||||
err.object = m_objects.front();
|
||||
@@ -3458,7 +3463,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
m_tool_ordering = ToolOrdering(*this, -1, false);
|
||||
m_tool_ordering.sort_and_build_data(*this, -1, false);
|
||||
if (m_tool_ordering.empty() || m_tool_ordering.last_extruder() == unsigned(-1))
|
||||
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
|
||||
throw Slic3r::SlicingError(L("The print is empty. The model is not printable with current print settings."));
|
||||
|
||||
}
|
||||
this->set_done(psWipeTower);
|
||||
@@ -5407,11 +5412,11 @@ void Print::_make_wipe_tower()
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("wipe tower footprint [%1%,%2%]-[%3%,%4%] leaves printable [%5%,%6%]-[%7%,%8%]") %
|
||||
unscaled(fp.min.x()) % unscaled(fp.min.y()) % unscaled(fp.max.x()) % unscaled(fp.max.y()) %
|
||||
unscaled(pr.min.x()) % unscaled(pr.min.y()) % unscaled(pr.max.x()) % unscaled(pr.max.y());
|
||||
throw Slic3r::SlicingError(L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n"));
|
||||
throw Slic3r::SlicingError(L("Prime tower is partially outside the printable area, and it cannot be printed.\n"));
|
||||
}
|
||||
// The cutter/purge corner is a physical obstacle — the brim must stay out like the body.
|
||||
if (!intersection(get_bed_excluded_area(m_config), Polygons{footprint}).empty())
|
||||
throw Slic3r::SlicingError(L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n"));
|
||||
throw Slic3r::SlicingError(L("Prime tower is too close to an exclusion area, and collisions will be caused.\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5526,7 +5531,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
||||
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
|
||||
}
|
||||
throw Slic3r::RuntimeError(
|
||||
std::string("Failed to process the G-code file ") + file + " from previous 3mf\n");
|
||||
// TRN %1% is a file path.
|
||||
Slic3r::format(L("Failed to process the G-code file %1% from the previous 3MF file."), file) + "\n");
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str();
|
||||
|
||||
+107
-96
@@ -2438,7 +2438,7 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("machine_end_gcode", coString);
|
||||
def->label = L("End G-code");
|
||||
def->tooltip = L("Add end G-Code when finishing the entire print.");
|
||||
def->tooltip = L("Add end G-code when finishing the entire print.");
|
||||
def->multiline = true;
|
||||
def->full_width = true;
|
||||
def->height = 12;
|
||||
@@ -4578,7 +4578,7 @@ void PrintConfigDef::init_fff_params()
|
||||
//def->enum_values.push_back("machinekit");
|
||||
//def->enum_values.push_back("smoothie");
|
||||
//def->enum_values.push_back("no-extrusion");
|
||||
def->enum_labels.push_back("Marlin(legacy)");
|
||||
def->enum_labels.push_back(L("Marlin (legacy)"));
|
||||
def->enum_labels.push_back(L("Klipper"));
|
||||
def->enum_labels.push_back("RepRapFirmware");
|
||||
def->enum_labels.push_back("Repetier");
|
||||
@@ -5301,11 +5301,11 @@ void PrintConfigDef::init_fff_params()
|
||||
(void)L("Maximum speed E");
|
||||
def->category = L("Machine limits");
|
||||
def->readonly = false;
|
||||
def->tooltip = (boost::format("Maximum speed of %1% axis") % axis_upper).str();
|
||||
(void)L("Maximum X speed");
|
||||
(void)L("Maximum Y speed");
|
||||
(void)L("Maximum Z speed");
|
||||
(void)L("Maximum E speed");
|
||||
def->tooltip = (boost::format("Maximum speed of the %1% axis") % axis_upper).str();
|
||||
(void)L("Maximum speed of the X axis");
|
||||
(void)L("Maximum speed of the Y axis");
|
||||
(void)L("Maximum speed of the Z axis");
|
||||
(void)L("Maximum speed of the E axis");
|
||||
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
|
||||
def->min = 0;
|
||||
def->mode = comSimple;
|
||||
@@ -5919,7 +5919,7 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("slicing_pipeline_plugin", coStrings);
|
||||
def->label = L("Slicing Pipeline Plugin");
|
||||
def->tooltip = L("Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, "
|
||||
def->tooltip = L("Python plugins invoked at each slicing pipeline step to read and modify intermediate slicing data, "
|
||||
"including a final G-code post-processing step. Research/experimental.");
|
||||
def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
|
||||
def->plugin_type = "slicing-pipeline";
|
||||
@@ -6943,7 +6943,7 @@ void PrintConfigDef::init_fff_params()
|
||||
// are unchanged; only the gcode emission frame shifts.
|
||||
def = this->add("imex_firmware_managed_zones", coBool);
|
||||
def->label = L("Firmware-managed zones");
|
||||
def->tooltip = L("When enabled, the slicer emits g-code with the primary zone's "
|
||||
def->tooltip = L("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 "
|
||||
@@ -7036,7 +7036,8 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("imex_head_filament_map", coString);
|
||||
def->label = L("IDEX/IQEX head filament map");
|
||||
def->tooltip = L("Per-plate override mapping physical heads to filament slots "
|
||||
"(1-based). Empty means use pem-inversion defaults.");
|
||||
"(1-based). Empty means each head uses the first filament that the "
|
||||
"printer's physical extruder map routes to it.");
|
||||
def->mode = comDevelop;
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
@@ -7628,7 +7629,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("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 "
|
||||
"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).");
|
||||
def->enum_keys_map = &ConfigOptionEnum<BeltRotationAxis>::get_enum_values();
|
||||
@@ -7653,7 +7654,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("belt_frame_tilt_decouple", coBool);
|
||||
def->label = L("Decouple machine-frame tilt");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Expert override: set the machine-frame (g-code shear/scale) tilt angle "
|
||||
def->tooltip = L("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 "
|
||||
@@ -8620,7 +8621,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Maximum wall deviation");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("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', "
|
||||
"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.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->mode = comExpert;
|
||||
@@ -9939,7 +9940,8 @@ void PrintConfigDef::handle_legacy_composite(DynamicPrintConfig &config)
|
||||
auto [thumbnails_list, errors] = GCodeThumbnails::make_and_check_thumbnail_list(thumbnails_str, extention);
|
||||
|
||||
if (errors != enum_bitmask<ThumbnailError>()) {
|
||||
std::string error_str = "\n" + Slic3r::format("Invalid value provided for parameter %1%: %2%", "thumbnails", thumbnails_str);
|
||||
// TRN %1% is a parameter name, %2% its value.
|
||||
std::string error_str = "\n" + Slic3r::format(_u8L("Invalid value provided for parameter %1%: %2%"), "thumbnails", thumbnails_str);
|
||||
error_str += GCodeThumbnails::get_error_string(errors);
|
||||
throw BadOptionValueException(error_str);
|
||||
}
|
||||
@@ -12328,42 +12330,42 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
std::map<std::string, std::string> error_message;
|
||||
// --layer-height
|
||||
if (cfg.get_abs_value("layer_height") <= 0) {
|
||||
error_message.emplace("layer_height", L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
|
||||
error_message.emplace("layer_height", _u8L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
|
||||
}
|
||||
else if (fabs(fmod(cfg.get_abs_value("layer_height"), SCALING_FACTOR)) > 1e-4) {
|
||||
error_message.emplace("layer_height", L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
|
||||
error_message.emplace("layer_height", _u8L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
|
||||
}
|
||||
|
||||
// --first-layer-height
|
||||
if (cfg.initial_layer_print_height.value <= 0) {
|
||||
error_message.emplace("initial_layer_print_height", L("invalid value ") + std::to_string(cfg.initial_layer_print_height.value));
|
||||
error_message.emplace("initial_layer_print_height", _u8L("invalid value ") + std::to_string(cfg.initial_layer_print_height.value));
|
||||
}
|
||||
|
||||
// --filament-diameter
|
||||
for (double fd : cfg.filament_diameter.values)
|
||||
if (fd < 1) {
|
||||
error_message.emplace("filament_diameter", L("invalid value ") + cfg.filament_diameter.serialize());
|
||||
error_message.emplace("filament_diameter", _u8L("invalid value ") + cfg.filament_diameter.serialize());
|
||||
break;
|
||||
}
|
||||
|
||||
// --nozzle-diameter
|
||||
for (double nd : cfg.nozzle_diameter.values)
|
||||
if (nd < 0.005) {
|
||||
error_message.emplace("nozzle_diameter", L("invalid value ") + cfg.nozzle_diameter.serialize());
|
||||
error_message.emplace("nozzle_diameter", _u8L("invalid value ") + cfg.nozzle_diameter.serialize());
|
||||
break;
|
||||
}
|
||||
|
||||
// --perimeters
|
||||
if (cfg.wall_loops.value < 0) {
|
||||
error_message.emplace("wall_loops", L("invalid value ") + std::to_string(cfg.wall_loops.value));
|
||||
error_message.emplace("wall_loops", _u8L("invalid value ") + std::to_string(cfg.wall_loops.value));
|
||||
}
|
||||
|
||||
// --solid-layers
|
||||
if (cfg.top_shell_layers < 0) {
|
||||
error_message.emplace("top_shell_layers", L("invalid value ") + std::to_string(cfg.top_shell_layers));
|
||||
error_message.emplace("top_shell_layers", _u8L("invalid value ") + std::to_string(cfg.top_shell_layers));
|
||||
}
|
||||
if (cfg.bottom_shell_layers < 0) {
|
||||
error_message.emplace("bottom_shell_layers", L("invalid value ") + std::to_string(cfg.bottom_shell_layers));
|
||||
error_message.emplace("bottom_shell_layers", _u8L("invalid value ") + std::to_string(cfg.bottom_shell_layers));
|
||||
}
|
||||
|
||||
if (cfg.use_firmware_retraction.value &&
|
||||
@@ -12384,61 +12386,61 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
|
||||
// --gcode-flavor
|
||||
if (! print_config_def.get("gcode_flavor")->has_enum_value(cfg.gcode_flavor.serialize())) {
|
||||
error_message.emplace("gcode_flavor", L("invalid value ") + cfg.gcode_flavor.serialize());
|
||||
error_message.emplace("gcode_flavor", _u8L("invalid value ") + cfg.gcode_flavor.serialize());
|
||||
}
|
||||
|
||||
// --fill-pattern
|
||||
if (! print_config_def.get("sparse_infill_pattern")->has_enum_value(cfg.sparse_infill_pattern.serialize())) {
|
||||
error_message.emplace("sparse_infill_pattern", L("invalid value ") + cfg.sparse_infill_pattern.serialize());
|
||||
error_message.emplace("sparse_infill_pattern", _u8L("invalid value ") + cfg.sparse_infill_pattern.serialize());
|
||||
}
|
||||
|
||||
// --top-fill-pattern
|
||||
if (! print_config_def.get("top_surface_pattern")->has_enum_value(cfg.top_surface_pattern.serialize())) {
|
||||
error_message.emplace("top_surface_pattern", L("invalid value ") + cfg.top_surface_pattern.serialize());
|
||||
error_message.emplace("top_surface_pattern", _u8L("invalid value ") + cfg.top_surface_pattern.serialize());
|
||||
}
|
||||
|
||||
// --bottom-fill-pattern
|
||||
if (! print_config_def.get("bottom_surface_pattern")->has_enum_value(cfg.bottom_surface_pattern.serialize())) {
|
||||
error_message.emplace("bottom_surface_pattern", L("invalid value ") + cfg.bottom_surface_pattern.serialize());
|
||||
error_message.emplace("bottom_surface_pattern", _u8L("invalid value ") + cfg.bottom_surface_pattern.serialize());
|
||||
}
|
||||
|
||||
// --soild-fill-pattern
|
||||
if (!print_config_def.get("internal_solid_infill_pattern")->has_enum_value(cfg.internal_solid_infill_pattern.serialize())) {
|
||||
error_message.emplace("internal_solid_infill_pattern", L("invalid value ") + cfg.internal_solid_infill_pattern.serialize());
|
||||
error_message.emplace("internal_solid_infill_pattern", _u8L("invalid value ") + cfg.internal_solid_infill_pattern.serialize());
|
||||
}
|
||||
|
||||
// --skirt-height
|
||||
if (cfg.skirt_height < 0) {
|
||||
error_message.emplace("skirt_height", L("invalid value ") + std::to_string(cfg.skirt_height));
|
||||
error_message.emplace("skirt_height", _u8L("invalid value ") + std::to_string(cfg.skirt_height));
|
||||
}
|
||||
|
||||
// --bridge-flow-ratio
|
||||
if (cfg.bridge_flow <= 0) {
|
||||
error_message.emplace("bridge_flow", L("invalid value ") + std::to_string(cfg.bridge_flow));
|
||||
error_message.emplace("bridge_flow", _u8L("invalid value ") + std::to_string(cfg.bridge_flow));
|
||||
}
|
||||
|
||||
// --internal-bridge-flow-ratio
|
||||
if (cfg.internal_bridge_flow <= 0) {
|
||||
error_message.emplace("internal_bridge_flow", L("invalid value ") + std::to_string(cfg.internal_bridge_flow));
|
||||
error_message.emplace("internal_bridge_flow", _u8L("invalid value ") + std::to_string(cfg.internal_bridge_flow));
|
||||
}
|
||||
|
||||
// extruder clearance
|
||||
if (cfg.extruder_clearance_radius <= 0) {
|
||||
error_message.emplace("extruder_clearance_radius", L("invalid value ") + std::to_string(cfg.extruder_clearance_radius));
|
||||
error_message.emplace("extruder_clearance_radius", _u8L("invalid value ") + std::to_string(cfg.extruder_clearance_radius));
|
||||
}
|
||||
if (cfg.extruder_clearance_height_to_rod <= 0) {
|
||||
error_message.emplace("extruder_clearance_height_to_rod", L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_rod));
|
||||
error_message.emplace("extruder_clearance_height_to_rod", _u8L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_rod));
|
||||
}
|
||||
if (cfg.extruder_clearance_height_to_lid <= 0) {
|
||||
error_message.emplace("extruder_clearance_height_to_lid", L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_lid));
|
||||
error_message.emplace("extruder_clearance_height_to_lid", _u8L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_lid));
|
||||
}
|
||||
if (cfg.nozzle_height <= 0)
|
||||
error_message.emplace("nozzle_height", L("invalid value ") + std::to_string(cfg.nozzle_height));
|
||||
error_message.emplace("nozzle_height", _u8L("invalid value ") + std::to_string(cfg.nozzle_height));
|
||||
|
||||
// --extrusion-multiplier
|
||||
for (double em : cfg.filament_flow_ratio.values)
|
||||
if (em <= 0) {
|
||||
error_message.emplace("filament_flow_ratio", L("invalid value ") + cfg.filament_flow_ratio.serialize());
|
||||
error_message.emplace("filament_flow_ratio", _u8L("invalid value ") + cfg.filament_flow_ratio.serialize());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -12448,27 +12450,27 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
// Note that we might want to have more than one perimeter on the bottom
|
||||
// solid layers.
|
||||
if (cfg.wall_loops != 1) {
|
||||
error_message.emplace("wall_loops", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.wall_loops));
|
||||
error_message.emplace("wall_loops", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.wall_loops));
|
||||
//return "Can't make more than one perimeter when spiral vase mode is enabled";
|
||||
//return "Can't make less than one perimeter when spiral vase mode is enabled";
|
||||
}
|
||||
|
||||
if (cfg.sparse_infill_density > 0) {
|
||||
error_message.emplace("sparse_infill_density", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.sparse_infill_density));
|
||||
error_message.emplace("sparse_infill_density", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.sparse_infill_density));
|
||||
//return "Spiral vase mode can only print hollow objects, so you need to set Fill density to 0";
|
||||
}
|
||||
|
||||
if (cfg.top_shell_layers > 0) {
|
||||
error_message.emplace("top_shell_layers", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.top_shell_layers));
|
||||
error_message.emplace("top_shell_layers", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.top_shell_layers));
|
||||
//return "Spiral vase mode is not compatible with top solid layers";
|
||||
}
|
||||
|
||||
if (cfg.enable_support ) {
|
||||
error_message.emplace("enable_support", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enable_support));
|
||||
error_message.emplace("enable_support", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enable_support));
|
||||
//return "Spiral vase mode is not compatible with support";
|
||||
}
|
||||
if (cfg.enforce_support_layers > 0) {
|
||||
error_message.emplace("enforce_support_layers", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enforce_support_layers));
|
||||
error_message.emplace("enforce_support_layers", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enforce_support_layers));
|
||||
//return "Spiral vase mode is not compatible with support";
|
||||
}
|
||||
}
|
||||
@@ -12499,9 +12501,9 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
double allowed_max = (key == "bridge_line_width") ? min_nozzle_diameter : MAX_LINE_WIDTH_MULTIPLIER * max_nozzle_diameter;
|
||||
if (abs_width > allowed_max) {
|
||||
if (key == "bridge_line_width")
|
||||
error_message.emplace(key, L("Bridge line width must not exceed nozzle diameter: ") + std::to_string(abs_width));
|
||||
error_message.emplace(key, _u8L("Bridge line width must not exceed nozzle diameter: ") + std::to_string(abs_width));
|
||||
else
|
||||
error_message.emplace(key, L("too large line width ") + std::to_string(abs_width));
|
||||
error_message.emplace(key, _u8L("too large line width ") + std::to_string(abs_width));
|
||||
//return std::string("Too Large line width: ") + key;
|
||||
}
|
||||
}
|
||||
@@ -12548,7 +12550,8 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
}
|
||||
if (out_of_range) {
|
||||
if (error_message.find(opt_key) == error_message.end())
|
||||
error_message.emplace(opt_key, opt->serialize() + L(" not in range ") +"[" + std::to_string(optdef->min) + "," + std::to_string(optdef->max) + "]");
|
||||
// TRN %1% is the current value, %2% and %3% the lowest and highest allowed values.
|
||||
error_message.emplace(opt_key, Slic3r::format(_u8L("%1% not in range [%2%, %3%]"), opt->serialize(), optdef->min, optdef->max));
|
||||
//return std::string("Value out of range: " + opt_key);
|
||||
}
|
||||
}
|
||||
@@ -12606,7 +12609,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
// Actions:
|
||||
/*def = this->add("export_obj", coBool);
|
||||
def->label = L("Export OBJ");
|
||||
def->tooltip = L("Export the model(s) as OBJ.");
|
||||
def->tooltip = L("Export the models as OBJ.");
|
||||
def->set_default_value(new ConfigOptionBool(false));*/
|
||||
|
||||
/*
|
||||
@@ -12642,7 +12645,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
|
||||
/*def = this->add("export_amf", coBool);
|
||||
def->label = L("Export AMF");
|
||||
def->tooltip = L("Export the model(s) as AMF.");
|
||||
def->tooltip = L("Export the models as AMF.");
|
||||
def->set_default_value(new ConfigOptionBool(false));*/
|
||||
|
||||
def = this->add("export_stl", coBool);
|
||||
@@ -12926,7 +12929,7 @@ CLITransformConfigDef::CLITransformConfigDef()
|
||||
|
||||
/*def = this->add("split", coBool);
|
||||
def->label = L("Split");
|
||||
def->tooltip = L("Detect unconnected parts in the given model(s) and split them into separate objects.");
|
||||
def->tooltip = L("Detect unconnected parts in the given models and split them into separate objects.");
|
||||
|
||||
def = this->add("scale_to_fit", coPoint3);
|
||||
def->label = L("Scale to Fit");
|
||||
@@ -13234,12 +13237,12 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef()
|
||||
def->label = L("Has single extruder MM priming");
|
||||
def->tooltip = L("Are the extra multi-material priming regions used in this print?");
|
||||
|
||||
new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool.");
|
||||
new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone.");
|
||||
new_def("curr_bed_type", coString, "Current bed type", "Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate').");
|
||||
new_def("imex_mode", coString, "IDEX/IQEX active mode", "Name of the active IDEX/IQEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IDEX/IQEX is not enabled.");
|
||||
new_def("imex_mode_index", coInt, "IDEX/IQEX active mode index", "Zero-based index of the active IDEX/IQEX parallel print mode within imex_mode_names.");
|
||||
new_def("imex_mode_gcode", coString, "IDEX/IQEX active mode G-code", "The raw mode G-code template for the active IDEX/IQEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode.");
|
||||
new_def("initial_no_support_extruder", coInt, L("Initial no support extruder"), L("Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool."));
|
||||
new_def("in_head_wrap_detect_zone", coBool, L("In head wrap detect zone"), L("Indicates if the first layer overlaps with the head wrap zone."));
|
||||
new_def("curr_bed_type", coString, L("Current bed type"), L("Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate')."));
|
||||
new_def("imex_mode", coString, L("IDEX/IQEX active mode"), L("Name of the active IDEX/IQEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IDEX/IQEX is not enabled."));
|
||||
new_def("imex_mode_index", coInt, L("IDEX/IQEX active mode index"), L("Zero-based index of the active IDEX/IQEX parallel print mode within imex_mode_names."));
|
||||
new_def("imex_mode_gcode", coString, L("IDEX/IQEX active mode G-code"), L("The raw mode G-code template for the active IDEX/IQEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."));
|
||||
}
|
||||
|
||||
PrintStatisticsConfigDef::PrintStatisticsConfigDef()
|
||||
@@ -13357,8 +13360,8 @@ ObjectsInfoConfigDef::ObjectsInfoConfigDef()
|
||||
def->label = L("Input filename without extension");
|
||||
def->tooltip = L("Source filename of the first object, without extension.");
|
||||
|
||||
new_def("input_filename", coString, "Full input filename", "Source filename of the first object.");
|
||||
new_def("plate_name", coString, "Plate name", "Name of the plate sliced.");
|
||||
new_def("input_filename", coString, L("Full input filename"), L("Source filename of the first object."));
|
||||
new_def("plate_name", coString, L("Plate name"), L("Name of the plate sliced."));
|
||||
}
|
||||
|
||||
DimensionsConfigDef::DimensionsConfigDef()
|
||||
@@ -13397,22 +13400,22 @@ DimensionsConfigDef::DimensionsConfigDef()
|
||||
def->label = L("Size of the print bed bounding box");
|
||||
def->tooltip = bb_size_tooltip;
|
||||
|
||||
new_def("first_layer_center_no_wipe_tower", coFloats, "First layer center without wipe tower", point_tooltip);
|
||||
new_def("first_layer_height", coFloat, "First layer height", "Height of the first layer.");
|
||||
new_def("first_layer_center_no_wipe_tower", coFloats, L("First layer center without wipe tower"), point_tooltip);
|
||||
new_def("first_layer_height", coFloat, L("First layer height"), L("Height of the first layer."));
|
||||
}
|
||||
|
||||
TemperaturesConfigDef::TemperaturesConfigDef()
|
||||
{
|
||||
ConfigOptionDef* def;
|
||||
|
||||
new_def("bed_temperature", coInts, "Bed temperature", "Vector of bed temperatures for each extruder/filament.")
|
||||
new_def("bed_temperature_initial_layer", coInts, "First layer bed temperature", "Vector of first layer bed temperatures for each extruder/filament. Provides the same value as first_layer_bed_temperature.")
|
||||
new_def("bed_temperature_initial_layer_single", coInt, "First layer bed temperature (initial extruder)", "First layer bed temperature for the initial extruder. Same as bed_temperature_initial_layer[initial_extruder]")
|
||||
new_def("chamber_temperature", coInts, "Chamber temperature", "Vector of chamber temperatures for each extruder/filament.")
|
||||
new_def("chamber_minimal_temperature", coInts, "Chamber minimal temperature", "Vector of minimal chamber temperatures for each extruder/filament.")
|
||||
new_def("overall_chamber_temperature", coInt, "Overall chamber temperature", "Overall chamber temperature. This value is the maximum chamber temperature of any extruder/filament used.")
|
||||
new_def("first_layer_bed_temperature", coInts, "First layer bed temperature", "Vector of first layer bed temperatures for each extruder/filament. Provides the same value as bed_temperature_initial_layer.")
|
||||
new_def("first_layer_temperature", coInts, "First layer temperature", "Vector of first layer temperatures for each extruder/filament.")
|
||||
new_def("bed_temperature", coInts, L("Bed temperature"), L("Vector of bed temperatures for each extruder/filament."))
|
||||
new_def("bed_temperature_initial_layer", coInts, L("First layer bed temperature"), L("Vector of first layer bed temperatures for each extruder/filament. Provides the same value as first_layer_bed_temperature."))
|
||||
new_def("bed_temperature_initial_layer_single", coInt, L("First layer bed temperature (initial extruder)"), L("First layer bed temperature for the initial extruder. Same as bed_temperature_initial_layer[initial_extruder]"))
|
||||
new_def("chamber_temperature", coInts, L("Chamber temperature"), L("Vector of chamber temperatures for each extruder/filament."))
|
||||
new_def("chamber_minimal_temperature", coInts, L("Chamber minimal temperature"), L("Vector of minimal chamber temperatures for each extruder/filament."))
|
||||
new_def("overall_chamber_temperature", coInt, L("Overall chamber temperature"), L("Overall chamber temperature. This value is the maximum chamber temperature of any extruder/filament used."))
|
||||
new_def("first_layer_bed_temperature", coInts, L("First layer bed temperature"), L("Vector of first layer bed temperatures for each extruder/filament. Provides the same value as bed_temperature_initial_layer."))
|
||||
new_def("first_layer_temperature", coInts, L("First layer temperature"), L("Vector of first layer temperatures for each extruder/filament."))
|
||||
}
|
||||
|
||||
|
||||
@@ -13519,42 +13522,50 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
|
||||
def->tooltip = L("The current extruder ID. The same as current_extruder.");
|
||||
|
||||
// change_filament_gcode
|
||||
new_def("previous_extruder", coInt, "Previous extruder", "Index of the extruder that is being unloaded. The index is zero based (first extruder has index 0).");
|
||||
new_def("next_extruder", coInt, "Next extruder", "Index of the extruder that is being loaded. The index is zero based (first extruder has index 0).");
|
||||
new_def("relative_e_axis", coBool, "Relative e-axis", "Indicates if relative positioning is being used.");
|
||||
new_def("toolchange_count", coInt, "Toolchange count", "The number of toolchanges throught the print.");
|
||||
new_def("previous_extruder", coInt, L("Previous extruder"), L("Index of the extruder that is being unloaded. The index is zero based (first extruder has index 0)."));
|
||||
new_def("next_extruder", coInt, L("Next extruder"), L("Index of the extruder that is being loaded. The index is zero based (first extruder has index 0)."));
|
||||
new_def("relative_e_axis", coBool, L("Relative e-axis"), L("Indicates if relative positioning is being used."));
|
||||
new_def("toolchange_count", coInt, L("Toolchange count"), L("The number of toolchanges throughout the print."));
|
||||
new_def("fan_speed", coNone, "", ""); //Option is no longer used and is zeroed by placeholder parser for compatability
|
||||
new_def("old_retract_length", coFloat, "Old retract length", "The retraction length of the previous filament.");
|
||||
new_def("new_retract_length", coFloat, "New retract length", "The retraction lenght of the new filament.");
|
||||
new_def("old_retract_length_toolchange", coFloat, "Old retract length toolchange", "The toolchange retraction length of the previous filament.");
|
||||
new_def("new_retract_length_toolchange", coFloat, "New retract length toolchange", "The toolchange retraction length of the new filament.");
|
||||
new_def("old_filament_temp", coInt, "Old filament temp", "The old filament temp.");
|
||||
new_def("new_filament_temp", coInt, "New filament temp", "The new filament temp.");
|
||||
new_def("x_after_toolchange", coFloat, "X after toolchange", "The X pos after toolchange.");
|
||||
new_def("y_after_toolchange", coFloat, "Y after toolchange", "The Y pos after toolchange.");
|
||||
new_def("z_after_toolchange", coFloat, "Z after toolchange", "The Z pos after toolchange.");
|
||||
new_def("first_flush_volume", coFloat, "First flush volume", "The first flush volume.");
|
||||
new_def("second_flush_volume", coFloat, "Second flush volume", "The second flush volume.");
|
||||
new_def("old_filament_e_feedrate", coInt, "Old filament e feedrate", "The old filament extruder feedrate.");
|
||||
new_def("new_filament_e_feedrate", coInt, "New filament e feedrate", "The new filament extruder feedrate.");
|
||||
new_def("travel_point_1_x", coFloat, "Travel point 1 X", "The travel point 1 X.");
|
||||
new_def("travel_point_1_y", coFloat, "Travel point 1 Y", "The travel point 1 Y.");
|
||||
new_def("travel_point_2_x", coFloat, "Travel point 2 X", "The travel point 2 X.");
|
||||
new_def("travel_point_2_y", coFloat, "Travel point 2 Y", "The travel point 2 Y.");
|
||||
new_def("travel_point_3_x", coFloat, "Travel point 3 X", "The travel point 3 X.");
|
||||
new_def("travel_point_3_y", coFloat, "Travel point 3 Y", "The travel point 3 Y.");
|
||||
new_def("flush_length_1", coFloat, "Flush Length 1", "The first flush length.");
|
||||
new_def("flush_length_2", coFloat, "Flush Length 2", "The second flush length.");
|
||||
new_def("flush_length_3", coFloat, "Flush Length 3", "The third flush length.");
|
||||
new_def("flush_length_4", coFloat, "Flush Length 4", "The fourth flush length.");
|
||||
new_def("old_retract_length", coFloat, L("Old retract length"), L("The retraction length of the previous filament."));
|
||||
new_def("new_retract_length", coFloat, L("New retract length"), L("The retraction length of the new filament."));
|
||||
new_def("old_retract_length_toolchange", coFloat, L("Old retract length toolchange"), L("The toolchange retraction length of the previous filament."));
|
||||
new_def("new_retract_length_toolchange", coFloat, L("New retract length toolchange"), L("The toolchange retraction length of the new filament."));
|
||||
new_def("old_filament_temp", coInt, L("Old filament temp"), L("The old filament temp."));
|
||||
new_def("new_filament_temp", coInt, L("New filament temp"), L("The new filament temp."));
|
||||
new_def("x_after_toolchange", coFloat, L("X after toolchange"), L("The X pos after toolchange."));
|
||||
new_def("y_after_toolchange", coFloat, L("Y after toolchange"), L("The Y pos after toolchange."));
|
||||
new_def("z_after_toolchange", coFloat, L("Z after toolchange"), L("The Z pos after toolchange."));
|
||||
new_def("first_flush_volume", coFloat, L("First flush volume"), L("The first flush volume."));
|
||||
new_def("second_flush_volume", coFloat, L("Second flush volume"), L("The second flush volume."));
|
||||
new_def("old_filament_e_feedrate", coInt, L("Old filament e feedrate"), L("The old filament extruder feedrate."));
|
||||
new_def("new_filament_e_feedrate", coInt, L("New filament e feedrate"), L("The new filament extruder feedrate."));
|
||||
new_def("travel_point_1_x", coFloat, L("Travel point 1 X"), L("The travel point 1 X."));
|
||||
new_def("travel_point_1_y", coFloat, L("Travel point 1 Y"), L("The travel point 1 Y."));
|
||||
new_def("travel_point_2_x", coFloat, L("Travel point 2 X"), L("The travel point 2 X."));
|
||||
new_def("travel_point_2_y", coFloat, L("Travel point 2 Y"), L("The travel point 2 Y."));
|
||||
new_def("travel_point_3_x", coFloat, L("Travel point 3 X"), L("The travel point 3 X."));
|
||||
new_def("travel_point_3_y", coFloat, L("Travel point 3 Y"), L("The travel point 3 Y."));
|
||||
new_def("flush_length_1", coFloat, L("Flush Length 1"), L("The first flush length."));
|
||||
new_def("flush_length_2", coFloat, L("Flush Length 2"), L("The second flush length."));
|
||||
new_def("flush_length_3", coFloat, L("Flush Length 3"), L("The third flush length."));
|
||||
new_def("flush_length_4", coFloat, L("Flush Length 4"), L("The fourth flush length."));
|
||||
|
||||
// change_extrusion_role_gcode
|
||||
std::string extrusion_role_types = "Possible Values:\n[\"Perimeter\", \"ExternalPerimeter\", "
|
||||
"\"OverhangPerimeter\", \"InternalInfill\", \"SolidInfill\", \"TopSolidInfill\", \"BottomSurface\", \"BridgeInfill\", \"GapFill\", \"Ironing\", "
|
||||
"\"Skirt\", \"Brim\", \"SupportMaterial\", \"SupportMaterialInterface\", \"SupportTransition\", \"WipeTower\", \"Mixed\"]";
|
||||
// The whole tooltip is one literal so that the GUI can look it up in the catalog.
|
||||
def = this->add("extrusion_role", coString);
|
||||
def->label = L("Extrusion role");
|
||||
// TRN The quoted values are G-code identifiers; keep them in English.
|
||||
def->tooltip = L("The new extrusion role/type that is going to be used\nPossible values:\n[\"Perimeter\", \"ExternalPerimeter\", "
|
||||
"\"OverhangPerimeter\", \"InternalInfill\", \"SolidInfill\", \"TopSolidInfill\", \"BottomSurface\", \"BridgeInfill\", \"GapFill\", \"Ironing\", "
|
||||
"\"Skirt\", \"Brim\", \"SupportMaterial\", \"SupportMaterialInterface\", \"SupportTransition\", \"WipeTower\", \"Mixed\"]");
|
||||
|
||||
new_def("extrusion_role", coString, "Extrusion role", "The new extrusion role/type that is going to be used\n" + extrusion_role_types);
|
||||
new_def("last_extrusion_role", coString, "Last extrusion role", "The previously used extrusion role/type\nPossible Values:\n" + extrusion_role_types);
|
||||
def = this->add("last_extrusion_role", coString);
|
||||
def->label = L("Last extrusion role");
|
||||
// TRN The quoted values are G-code identifiers; keep them in English.
|
||||
def->tooltip = L("The previously used extrusion role/type\nPossible values:\n[\"Perimeter\", \"ExternalPerimeter\", "
|
||||
"\"OverhangPerimeter\", \"InternalInfill\", \"SolidInfill\", \"TopSolidInfill\", \"BottomSurface\", \"BridgeInfill\", \"GapFill\", \"Ironing\", "
|
||||
"\"Skirt\", \"Brim\", \"SupportMaterial\", \"SupportMaterialInterface\", \"SupportTransition\", \"WipeTower\", \"Mixed\"]");
|
||||
}
|
||||
|
||||
const CustomGcodeSpecificConfigDef custom_gcode_specific_config_def;
|
||||
|
||||
@@ -885,7 +885,8 @@ void PrintObject::contour_z()
|
||||
|
||||
std::scoped_lock lock(mtx);
|
||||
completed++;
|
||||
std::string msg = (boost::format("Z contoured layer %d/%d (%d%%)") % (completed) % m_layers.size() % int(double(completed) / m_layers.size() * 100)).str();
|
||||
// TRN Slicing status: layers done, total layers, percentage done.
|
||||
std::string msg = (boost::format(L("Z contoured layer %d/%d (%d%%)")) % (completed) % m_layers.size() % int(double(completed) / m_layers.size() * 100)).str();
|
||||
m_print->set_status(40, msg);
|
||||
}
|
||||
}
|
||||
@@ -958,12 +959,11 @@ void PrintObject::generate_support_material(bool with_belt_brim)
|
||||
BOOST_LOG_TRIVIAL(info) << std::fixed << std::setprecision(0) << "is_support_necessary takes " << duration << " secs.";
|
||||
|
||||
if (sntype != NoNeedSupp) {
|
||||
std::map<SupportNecessaryType, std::string> reasons = {
|
||||
{SharpTail,L("floating regions")},
|
||||
{Cantilever,L("floating cantilever")},
|
||||
{LargeOverhang,L("large overhangs")} };
|
||||
std::string warning_message = Slic3r::format(L("It seems object %s has %s. Please re-orient the object or enable support generation."),
|
||||
this->model_object()->name, reasons[sntype]);
|
||||
std::map<SupportNecessaryType, std::string> messages = {
|
||||
{SharpTail,L("It seems object %1% has floating regions. Please re-orient the object or enable support generation.")},
|
||||
{Cantilever,L("It seems object %1% has a floating cantilever. Please re-orient the object or enable support generation.")},
|
||||
{LargeOverhang,L("It seems object %1% has large overhangs. Please re-orient the object or enable support generation.")} };
|
||||
std::string warning_message = Slic3r::format(messages[sntype], this->model_object()->name);
|
||||
this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_message, PrintStateBase::SlicingNeedSupportOn);
|
||||
}
|
||||
|
||||
|
||||
@@ -956,7 +956,7 @@ void PrintObject::slice()
|
||||
}
|
||||
});
|
||||
if (m_layers.empty())
|
||||
throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n"));
|
||||
throw Slic3r::SlicingError(L("No layers were detected. You might want to repair the model or check its size or thickness and retry.\n"));
|
||||
|
||||
// Belt printer: offset all layer Z values so objects at different positions
|
||||
// along the belt print at different heights on the tilted belt. This is a
|
||||
|
||||
@@ -263,7 +263,7 @@ std::string MZ_Archive::get_errorstr(mz_zip_error mz_err)
|
||||
break;
|
||||
}
|
||||
|
||||
return "unknown error";
|
||||
return L("unknown error");
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1948,7 +1948,7 @@ void copy_directory_recursively(const boost::filesystem::path& source,
|
||||
if (cfr != CopyFileResult::SUCCESS) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Copying failed(" << cfr << "): " << error_message;
|
||||
throw Slic3r::CriticalException(Slic3r::format(
|
||||
("Copying directory %1% to %2% failed: %3%"),
|
||||
_u8L("Copying directory %1% to %2% failed: %3%"),
|
||||
source, target, error_message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent)
|
||||
m_time_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
m_time_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK));
|
||||
|
||||
Label* time_unit_label = new Label(parent, "H");
|
||||
Label* time_unit_label = new Label(parent, _L_CONTEXT("H", "Hours unit"));
|
||||
time_unit_label->SetForegroundColour(*wxBLACK);
|
||||
time_sizer->Add(m_time_input, 1, wxRIGHT, FromDIP(1));
|
||||
time_sizer->Add(time_unit_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
@@ -974,12 +974,12 @@ void AuxiliaryPanel::on_import_file(wxCommandEvent &event)
|
||||
|
||||
if (file_model == s_default_folders[MODEL_PICTURE]) {
|
||||
//wildcard = wxT("JPEG files (*.jpeg)|*.jpeg|BMP files (*.bmp)|*.bmp|GIF files (*.gif)|*.gif|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg");
|
||||
wildcard = wxT("files (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp");
|
||||
wildcard = _L("Image files") + " (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";
|
||||
}
|
||||
|
||||
if (file_model == s_default_folders[OTHERS]) { wildcard = wxT("TXT files (*.txt)|*.txt"); }
|
||||
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = wxT("EXCEL files (*.xls)|*.xls|EXCEL files (*.xlsx)|*.xlsx|PDF files (*.pdf)|*.pdf"); }
|
||||
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = wxT("PDF files (*.pdf)|*.pdf"); }
|
||||
if (file_model == s_default_folders[OTHERS]) { wildcard = _L("TXT files") + " (*.txt)|*.txt"; }
|
||||
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = _L("EXCEL files") + " (*.xls)|*.xls|" + _L("EXCEL files") + " (*.xlsx)|*.xlsx|" + _L("PDF files") + " (*.pdf)|*.pdf"; }
|
||||
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = _L("PDF files") + " (*.pdf)|*.pdf"; }
|
||||
|
||||
wxFileDialog dialog(this, _L("Choose files"), wxEmptyString, wxEmptyString, wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE);
|
||||
if (dialog.ShowModal() == wxID_OK) {
|
||||
|
||||
@@ -122,7 +122,7 @@ std::pair<std::string, std::vector<size_t>> SlicingProcessCompletedEvent::format
|
||||
} catch (std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown C++ exception.";
|
||||
error = _u8L("Unknown C++ exception.");
|
||||
}
|
||||
return std::make_pair(std::move(error), std::vector<size_t>{monospace});
|
||||
}
|
||||
|
||||
@@ -931,7 +931,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
});
|
||||
|
||||
feat_dropdown("plane", "design_plane", _L("Datum / Curve (plane / axis / coord sys / helix / project)"), {
|
||||
{"design_plane", _L("Plane"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
|
||||
{"design_plane", _L_CONTEXT("Plane", "Design"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
|
||||
[this] {
|
||||
populate_plane_choices(m_plane_base);
|
||||
reset_plane_refs();
|
||||
@@ -1011,7 +1011,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
open_tool(Tool::Transform);
|
||||
}, SHIFT('Y')},
|
||||
{"design_mirror", _L("Mirror"), _L("Reflect a body about a plane"),
|
||||
{"design_mirror", _L_CONTEXT("Mirror", "Design"), _L("Reflect a body about a plane"),
|
||||
[this] {
|
||||
if (m_doc.bodies.empty()) {
|
||||
set_status(StatusKind::Error, _L("Mirror needs a body — add or import one first"));
|
||||
@@ -1143,7 +1143,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
// Hole / Thread — drilling into a solid (both face-aware)
|
||||
feat_dropdown("hole", "design_hole", _L("Hole / thread"), {
|
||||
{"design_hole", _L("Hole"), _L("Drill a hole, centered on a picked face or placed on a plane"),
|
||||
{"design_hole", _L_CONTEXT("Hole", "Design"), _L("Drill a hole, centered on a picked face or placed on a plane"),
|
||||
[this] {
|
||||
// #2: drill on the picked solid face, centred on it (origin = face centroid,
|
||||
// normal = inward). Otherwise fall back to the plane dropdown. m_hole_x/y then
|
||||
@@ -1344,7 +1344,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
_L("Click 2 points or a line / circle / arc to place a dimension"));
|
||||
// (separator dropped: the group it divided is now reached from the offer)
|
||||
dropdown("design_line", _L("Line / polyline"), {
|
||||
{"design_line", DesignSketchTool::Mode::Line, _L("Line"), _L("Click start, then end — then set the exact length")},
|
||||
{"design_line", DesignSketchTool::Mode::Line, _L_CONTEXT("Line", "Design"), _L("Click start, then end — then set the exact length")},
|
||||
{"design_polyline", DesignSketchTool::Mode::Polyline, _L("Polyline"), _L("Click points; click the first point to close the loop, right-click or Enter to end the chain")} });
|
||||
dropdown("design_rect", _L("Rectangle"), {
|
||||
{"design_rect", DesignSketchTool::Mode::CornerRect, _L("Corner rectangle"), _L("Click two opposite corners")},
|
||||
@@ -1407,7 +1407,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
{"design_chamfer", DesignSketchTool::Mode::Chamfer, _L("Chamfer"), _L("Pick two lines, then drag the arrow or click the distance to set it")} });
|
||||
skbtn("design_offset", DesignSketchTool::Mode::Offset, _L("Offset"),
|
||||
_L("Pick an entity, then drag the arrow or click the distance; click empty to apply"));
|
||||
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L("Mirror"),
|
||||
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L_CONTEXT("Mirror", "Design"),
|
||||
_L("Pick a mirror-axis line, then the entities to mirror; click empty to apply"));
|
||||
// Trim / Extend scissors — standalone sketch tools (NOT inside Constrain): click a
|
||||
// segment to cut it back to / out to its nearest intersection. One cut per click.
|
||||
@@ -1438,9 +1438,12 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
auto arm_polygon = [this, select_tool] {
|
||||
push_polygon_params();
|
||||
select_tool(DesignSketchTool::Mode::Polygon,
|
||||
wxString::Format(_L("Click center then a vertex — %d sides, %s"),
|
||||
m_poly_sides,
|
||||
m_poly_circumscribed ? _L("circumscribed") : _L("inscribed")));
|
||||
wxString::Format(m_poly_circumscribed
|
||||
? _L_PLURAL("Click center then a vertex — %d side, circumscribed",
|
||||
"Click center then a vertex — %d sides, circumscribed", m_poly_sides)
|
||||
: _L_PLURAL("Click center then a vertex — %d side, inscribed",
|
||||
"Click center then a vertex — %d sides, inscribed", m_poly_sides),
|
||||
m_poly_sides));
|
||||
};
|
||||
for (int n : {3, 4, 5, 6, 8, 12})
|
||||
m_verb_actions["btn:poly#" + std::to_string(n)] =
|
||||
@@ -1998,7 +2001,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
hform->Add(m_hole_target_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
m_box_hole = new wxBoxSizer(wxVERTICAL);
|
||||
m_box_hole->Add(card_header(m_cards, "design_hole", _L("Hole"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_hole->Add(card_header(m_cards, "design_hole", _L_CONTEXT("Hole", "Design"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_hole->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
|
||||
m_box_hole->Add(hform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
cards->Add(m_box_hole, 0, wxEXPAND);
|
||||
@@ -2022,7 +2025,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_thread_std->Append(s.name);
|
||||
m_thread_std->SetSelection(0);
|
||||
m_thread_std->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { apply_thread_standard(); });
|
||||
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Standard")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Standard", "Thread standard")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(m_thread_std, 0, wxEXPAND);
|
||||
|
||||
// Threads are specified by DIAMETER (M6 = Ø6); the value is derived from the picked cylindrical
|
||||
@@ -2233,7 +2236,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
m_cut_plane = make_combo(m_cards);
|
||||
m_cut_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
|
||||
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
cform->Add(m_cut_plane, 0, wxEXPAND);
|
||||
|
||||
m_cut_offset = make_spin(m_cards, 0.0, -10000.0, 10000.0);
|
||||
@@ -2258,7 +2261,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
// --- Plane (datum/reference plane: offset + tilt from a base plane; no solid) ---
|
||||
m_box_plane = new wxBoxSizer(wxVERTICAL);
|
||||
m_box_plane->Add(card_header(m_cards, "design_plane", _L("Plane"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(card_header(m_cards, "design_plane", _L_CONTEXT("Plane", "Design"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
|
||||
{
|
||||
// Plane type chooses which inputs matter (Onshape/Fusion parity):
|
||||
@@ -2570,7 +2573,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
// --- Mirror (reflect a body about a plane) ---
|
||||
m_box_mirror = new wxBoxSizer(wxVERTICAL);
|
||||
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L("Mirror"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L_CONTEXT("Mirror", "Design"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_mirror->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
|
||||
{
|
||||
auto* mform = two_col_form();
|
||||
@@ -2748,7 +2751,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_helix_plane = make_combo(m_cards);
|
||||
populate_plane_choices(m_helix_plane);
|
||||
m_helix_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
|
||||
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
hform->Add(m_helix_plane, 0, wxEXPAND);
|
||||
m_helix_radius = make_spin(m_cards, 10.0, 0.01, 10000.0);
|
||||
m_helix_radius->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); });
|
||||
@@ -3556,7 +3559,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_doc.recompute();
|
||||
set_status(StatusKind::Info, cons.empty()
|
||||
? _L("Sketch created — select it, then right-click to Extrude")
|
||||
: wxString::Format(_L("Sketch created (%zu driving dims) — select it, then right-click to Extrude"),
|
||||
: wxString::Format(_L_PLURAL("Sketch created (%zu driving dim) — select it, then right-click to Extrude",
|
||||
"Sketch created (%zu driving dims) — select it, then right-click to Extrude",
|
||||
unsigned(cons.size())),
|
||||
cons.size()));
|
||||
refresh_tree();
|
||||
sync_sketch_display(); // keep the just-committed sketch visible as a face
|
||||
@@ -4585,8 +4590,8 @@ void DesignPanel::on_shape_changed()
|
||||
|
||||
void DesignPanel::set_status_ok()
|
||||
{
|
||||
set_status(wxString::Format(_L("OK — %zu triangles"),
|
||||
m_doc.display_mesh.its.indices.size()));
|
||||
const size_t n_tris = m_doc.display_mesh.its.indices.size();
|
||||
set_status(wxString::Format(_L_PLURAL("OK — %zu triangle", "OK — %zu triangles", unsigned(n_tris)), n_tris));
|
||||
if (m_viewport != nullptr) {
|
||||
// A recompute invalidates the gizmo's body centroid; a Move ends with it, keeping its pose.
|
||||
if (body_move_pending()) end_body_move(true);
|
||||
@@ -5035,11 +5040,15 @@ void DesignPanel::on_import_mesh()
|
||||
// A dense organic scan has few coplanar neighbours to merge away and stays heavy afterwards;
|
||||
// warn rather than silently freezing the CAD kernel on every subsequent recompute.
|
||||
if (mesh.its.indices.size() > MESH_IMPORT_TRIANGLE_WARN) {
|
||||
const int n_tris = int(mesh.its.indices.size());
|
||||
const wxString q = wxString::Format(
|
||||
_L("This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
|
||||
"merging, so importing it may take a long time and leave a body that is slow to "
|
||||
"edit. Decimating the mesh first is usually better.\n\nImport anyway?"),
|
||||
int(mesh.its.indices.size()));
|
||||
_L_PLURAL("This mesh has %d triangle. Every triangle becomes a B-rep face before coplanar "
|
||||
"merging, so importing it may take a long time and leave a body that is slow to "
|
||||
"edit. Decimating the mesh first is usually better.\n\nImport anyway?",
|
||||
"This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
|
||||
"merging, so importing it may take a long time and leave a body that is slow to "
|
||||
"edit. Decimating the mesh first is usually better.\n\nImport anyway?", n_tris),
|
||||
n_tris);
|
||||
MessageDialog dlg(this, q, _L("Large mesh"), wxYES_NO | wxICON_WARNING);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return;
|
||||
@@ -5080,7 +5089,7 @@ void DesignPanel::on_import_mesh()
|
||||
// source mesh the user needs to know about before they start cutting features into it.
|
||||
if (stats.is_solid) {
|
||||
set_status(StatusKind::Info, wxString::Format(
|
||||
_L("Imported solid — %d triangles → %d faces, volume %.2f mm³. Pick a face or edge, "
|
||||
_L("Imported solid — triangles: %d → faces: %d, volume %.2f mm³. Pick a face or edge, "
|
||||
"then Fillet / Cut / Shell to modify"),
|
||||
stats.kept_tris, stats.faces_final, stats.volume));
|
||||
} else {
|
||||
@@ -5171,7 +5180,8 @@ void DesignPanel::open_insert_card(const wxString& base_name)
|
||||
update_cards_frame(); m_form->Layout();
|
||||
m_form->FitInside();
|
||||
update_action_bar(); // surface the unified ✓/✗
|
||||
set_status(StatusKind::Info, base_name + _L(" — drag to place/size, then Confirm"));
|
||||
// TRN Design tab status; %s is the inserted item, such as "Text" or "SVG".
|
||||
set_status(StatusKind::Info, wxString::Format(_L("%s — drag to place/size, then Confirm"), base_name));
|
||||
}
|
||||
|
||||
// Confirm: keep the placed art and leave the placement gizmo. The feature is already in
|
||||
@@ -5255,7 +5265,7 @@ void DesignPanel::on_add_extrude()
|
||||
{
|
||||
BooleanMode mode = static_cast<BooleanMode>(m_mode->GetSelection()); // New/Add/Cut/Intersect
|
||||
m_feature_counter++;
|
||||
const std::string name = feature_name(_L("Extrude"));
|
||||
const std::string name = feature_name(_L_CONTEXT("Extrude", "Design"));
|
||||
int idx = -1;
|
||||
if (m_extrude_face_src >= 0) {
|
||||
// Onshape face-extrude: the picked solid face is the profile (no sketch wire).
|
||||
@@ -5383,7 +5393,7 @@ void DesignPanel::on_add_hole()
|
||||
|
||||
m_feature_counter++;
|
||||
const int hidx = m_doc.add_hole(dia, depth, through, px, py, plane,
|
||||
feature_name(_L("Hole")));
|
||||
feature_name(_L_CONTEXT("Hole", "Design")));
|
||||
// On-face holes drill the body the face belongs to (even after the pick was cleared).
|
||||
if (m_hole_on_face && hidx >= 0 && hidx < int(m_doc.features.size()))
|
||||
m_doc.features[hidx].target_body = m_hole_face_body;
|
||||
@@ -5770,7 +5780,7 @@ void DesignPanel::on_add_mirror()
|
||||
const BooleanMode mode = m_mirror_keep->GetValue() ? BooleanMode::New : BooleanMode::Add;
|
||||
m_feature_counter++;
|
||||
m_doc.add_mirror(plane_from_choice(m_mirror_plane->GetSelection()), target, mode,
|
||||
feature_name(_L("Mirror")));
|
||||
feature_name(_L_CONTEXT("Mirror", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -5817,7 +5827,7 @@ void DesignPanel::on_add_rib()
|
||||
}
|
||||
m_feature_counter++;
|
||||
m_doc.add_rib(sketch_ref, m_rib_entity->GetValue(), m_rib_thickness->GetValue(),
|
||||
m_rib_depth->GetValue(), target, feature_name(_L("Rib")));
|
||||
m_rib_depth->GetValue(), target, feature_name(_L_CONTEXT("Rib", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -5837,7 +5847,7 @@ void DesignPanel::on_add_project()
|
||||
m_feature_counter++;
|
||||
m_doc.add_project_edges(src_body, {}, face,
|
||||
plane_from_choice(m_proj_plane->GetSelection()),
|
||||
feature_name(_L("Project")));
|
||||
feature_name(_L_CONTEXT("Project", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -6024,7 +6034,7 @@ void DesignPanel::on_add_pattern()
|
||||
m_doc.add_pattern(circular, int(m_pattern_count->GetValue()),
|
||||
m_pattern_spacing->GetValue(), m_pattern_dir->GetSelection(),
|
||||
m_pattern_angle->GetValue(), target,
|
||||
feature_name(_L("Pattern")));
|
||||
feature_name(_L_CONTEXT("Pattern", "Design")));
|
||||
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
@@ -6126,7 +6136,7 @@ void DesignPanel::on_add_cut()
|
||||
m_feature_counter++;
|
||||
m_doc.add_cut(plane_from_choice(m_cut_plane->GetSelection()), m_cut_offset->GetValue(),
|
||||
/*flip*/ false, /*keep_upper*/ true, /*keep_lower*/ true,
|
||||
m_cut_target->GetSelection(), feature_name(_L("Cut")));
|
||||
m_cut_target->GetSelection(), feature_name(_L_CONTEXT("Cut", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -6429,7 +6439,8 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
|
||||
using Mode = DesignSketchTool::Mode;
|
||||
auto pick_more = [](const wxString& lead, int n) {
|
||||
return n <= 0 ? lead
|
||||
: lead + wxString::Format(_L(" · %d picked"), n);
|
||||
// TRN Appended to a sketch tool prompt; %d is the number of picked entities.
|
||||
: lead + wxString::Format(_L_PLURAL(" · %d picked", " · %d picked", n), n);
|
||||
};
|
||||
switch (m) {
|
||||
case Mode::Select:
|
||||
@@ -6442,8 +6453,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
|
||||
"Shift-click adds · Del removes");
|
||||
case Mode::Constrain:
|
||||
return picks > 0
|
||||
? wxString::Format(_L("%d picked · now choose a constraint (Horizontal, Parallel, "
|
||||
"Tangent, Equal…)"), picks)
|
||||
? wxString::Format(_L_PLURAL("%d picked · now choose a constraint (Horizontal, Parallel, "
|
||||
"Tangent, Equal…)",
|
||||
"%d picked · now choose a constraint (Horizontal, Parallel, "
|
||||
"Tangent, Equal…)", picks), picks)
|
||||
: _L("Constrain — click one or two entities, then choose a constraint");
|
||||
case Mode::Dimension:
|
||||
return step == 0 ? _L("Dimension — click an entity, or the first of two points")
|
||||
@@ -6544,8 +6557,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
|
||||
? _L("Mirror — first click the LINE to mirror about (a construction line works)")
|
||||
: picks == 0
|
||||
? _L("Mirror — axis set · now click the entities to mirror · Esc cancels")
|
||||
: wxString::Format(_L("Mirror — axis set · %d to mirror · click another to add or "
|
||||
"remove it · Enter applies"), picks);
|
||||
: wxString::Format(_L_PLURAL("Mirror — axis set · %d to mirror · click another to add or "
|
||||
"remove it · Enter applies",
|
||||
"Mirror — axis set · %d to mirror · click another to add or "
|
||||
"remove it · Enter applies", picks), picks);
|
||||
case Mode::Move:
|
||||
return step == 0 ? _L("Move — click the entities to move")
|
||||
: pick_more(_L("Move — drag the handle, or click the number to type the "
|
||||
@@ -6653,7 +6668,7 @@ wxString DesignPanel::offer_header(int kind) const
|
||||
case OfferSel::SkPoint: return _L("Sketch point");
|
||||
case OfferSel::Sk2Ent: {
|
||||
const int n = m_viewport ? m_viewport->sketch_selection_count() : 0;
|
||||
return wxString::Format(_L("%d sketch entities"), n);
|
||||
return wxString::Format(_L_PLURAL("%d sketch entity", "%d sketch entities", n), n);
|
||||
}
|
||||
default: return wxString();
|
||||
}
|
||||
@@ -6753,7 +6768,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
} else {
|
||||
offer_trace("row=%d %s DISABLED (%s)", row, kOfferRowNames[row],
|
||||
v->refusal ? v->refusal : "blocked");
|
||||
wxString s = tr(v->name);
|
||||
wxString s = I18N::translate(v->name, "Design");
|
||||
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
|
||||
append_offer_item(&menu, id, s, *v)->Enable(false);
|
||||
bound.push_back(nullptr);
|
||||
@@ -6828,7 +6843,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
}
|
||||
if (!ok) {
|
||||
offer_trace("row=%d %s ~ %s BLOCKED", row, kOfferRowNames[row], v->id);
|
||||
wxString s = tr(v->name);
|
||||
wxString s = I18N::translate(v->name, "Design");
|
||||
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
|
||||
append_offer_item(target, base + int(bound.size()), s, *v)->Enable(false);
|
||||
bound.push_back(nullptr);
|
||||
@@ -6935,8 +6950,8 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return;
|
||||
drop_ghost(); // the real bodies are about to become the ghost's pose
|
||||
m_doc.checkpoint(); // undo boundary: committing a mate from the offer
|
||||
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false,
|
||||
"Mate" + std::to_string(++m_feature_counter));
|
||||
++m_feature_counter;
|
||||
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false, feature_name(_L("Mate")));
|
||||
if (idx < 0) {
|
||||
set_status(StatusKind::Error, _L("Mate rejected"));
|
||||
return;
|
||||
@@ -7165,7 +7180,7 @@ bool DesignPanel::on_add_plane()
|
||||
m_feature_counter++;
|
||||
int idx = m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
|
||||
m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(),
|
||||
feature_name(_L("Plane")));
|
||||
feature_name(_L_CONTEXT("Plane", "Design")));
|
||||
if (idx >= 0 && idx < int(m_doc.features.size())) apply_plane_refs(m_doc.features[idx]);
|
||||
m_doc.recompute(); // datum-only docs yield no body; that is expected/benign
|
||||
set_status(StatusKind::Info, _L("Plane added — pick it as a sketch plane"));
|
||||
@@ -7206,7 +7221,7 @@ void DesignPanel::on_add_shell()
|
||||
|
||||
m_feature_counter++;
|
||||
m_doc.add_shell(m_shell_thickness->GetValue(), face, m_sel_solid_body,
|
||||
feature_name(_L("Shell")));
|
||||
feature_name(_L_CONTEXT("Shell", "Design")));
|
||||
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
@@ -9500,7 +9515,8 @@ void DesignPanel::request_value(const wxString& label, double def, double mn, do
|
||||
m_form->FitInside();
|
||||
m_value_input->SetFocus();
|
||||
m_value_input->SetSelection(-1, -1); // select all so typing replaces the value
|
||||
set_status(StatusKind::Info, label + _L(" — type a value, press Enter (or Confirm)"));
|
||||
// TRN Design tab status; %s is the value being asked for, such as "Fillet radius".
|
||||
set_status(StatusKind::Info, wxString::Format(_L("%s — type a value, press Enter (or Confirm)"), label));
|
||||
}
|
||||
|
||||
void DesignPanel::confirm_value()
|
||||
@@ -10228,19 +10244,19 @@ void DesignPanel::on_commit()
|
||||
for (size_t b = 0; b < m_disp_body_meshes.size(); ++b) {
|
||||
if (b < m_body_visible.size() && !m_body_visible[b]) continue; // skip hidden
|
||||
if (m_disp_body_meshes[b].its.indices.empty()) continue;
|
||||
parts.emplace_back(&m_disp_body_meshes[b], wxString::FromUTF8("Design Body " + std::to_string(b + 1)));
|
||||
parts.emplace_back(&m_disp_body_meshes[b], wxString::Format(_L("Design Body %d"), int(b + 1)));
|
||||
}
|
||||
if (parts.empty()) { // every body hidden — nothing to ship
|
||||
set_status(_L("All bodies hidden — show one before committing"));
|
||||
return;
|
||||
}
|
||||
if (m_commit_mode == CommitMode::Assembly && parts.size() > 1)
|
||||
obj_list->load_mesh_object(parts, "Design Assembly");
|
||||
obj_list->load_mesh_object(parts, _L("Design Assembly"));
|
||||
else
|
||||
for (const auto& [mesh, name] : parts)
|
||||
obj_list->load_mesh_object(*mesh, name);
|
||||
} else {
|
||||
obj_list->load_mesh_object(m_disp_pick_mesh, "Design Body");
|
||||
obj_list->load_mesh_object(m_disp_pick_mesh, _L("Design Body"));
|
||||
}
|
||||
|
||||
// Persist the editable parametric recipe alongside the committed meshes so the
|
||||
@@ -11478,7 +11494,8 @@ void DesignPanel::refresh_preview()
|
||||
|
||||
if (ok) {
|
||||
m_viewport->set_preview_mesh(mesh);
|
||||
set_status(StatusKind::Ok, wxString::Format(_L("Preview — %zu triangles"), mesh.its.indices.size()));
|
||||
const size_t n_tris = mesh.its.indices.size();
|
||||
set_status(StatusKind::Ok, wxString::Format(_L_PLURAL("Preview — %zu triangle", "Preview — %zu triangles", unsigned(n_tris)), n_tris));
|
||||
} else {
|
||||
m_viewport->clear_preview();
|
||||
set_status(StatusKind::Error, wxString::Format(_L("Invalid: %s"), kernel_error_text(err)));
|
||||
@@ -11789,13 +11806,13 @@ void DesignPanel::open_tool(Tool t)
|
||||
case Tool::Extrude: m_hdr_extrude->SetLabel(title(_L_CONTEXT("Extrude", "Design"))); break;
|
||||
case Tool::Dressup: m_hdr_dressup->SetLabel(title(
|
||||
m_dressup_type->GetSelection() == 0 ? _L("Fillet") : _L("Chamfer"))); break;
|
||||
case Tool::Hole: m_hdr_hole->SetLabel(title(_L("Hole"))); break;
|
||||
case Tool::Hole: m_hdr_hole->SetLabel(title(_L_CONTEXT("Hole", "Design"))); break;
|
||||
case Tool::Thread: m_hdr_thread->SetLabel(title(_L("Thread"))); break;
|
||||
case Tool::Shell: m_hdr_shell->SetLabel(title(_L_CONTEXT("Shell", "Design"))); break;
|
||||
case Tool::Revolve: m_hdr_revolve->SetLabel(title(_L("Revolve"))); break;
|
||||
case Tool::Sweep: m_hdr_sweep->SetLabel(title(_L("Sweep"))); break;
|
||||
case Tool::Pattern: m_hdr_pattern->SetLabel(title(_L_CONTEXT("Pattern", "Design"))); break;
|
||||
case Tool::Plane: m_hdr_plane->SetLabel(title(_L("Plane"))); break;
|
||||
case Tool::Plane: m_hdr_plane->SetLabel(title(_L_CONTEXT("Plane", "Design"))); break;
|
||||
case Tool::Loft: m_hdr_loft->SetLabel(title(_L("Loft"))); break;
|
||||
case Tool::Draft: m_hdr_draft->SetLabel(title(_L("Draft"))); break;
|
||||
case Tool::Boolean: m_hdr_boolean->SetLabel(title(_L("Boolean"))); break;
|
||||
@@ -11809,7 +11826,7 @@ void DesignPanel::open_tool(Tool t)
|
||||
case Tool::SurfaceOffset: m_hdr_surf_offset->SetLabel(title(_L("Surface Offset"))); break;
|
||||
case Tool::ThickenSurface: m_hdr_surf_thicken->SetLabel(title(_L("Thicken Surface"))); break;
|
||||
case Tool::Transform: m_hdr_transform->SetLabel(title(_L("Transform"))); break;
|
||||
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L("Mirror"))); break;
|
||||
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L_CONTEXT("Mirror", "Design"))); break;
|
||||
case Tool::Thicken: m_hdr_thicken->SetLabel(title(_L("Thicken"))); break;
|
||||
case Tool::Rib: m_hdr_rib->SetLabel(title(_L_CONTEXT("Rib", "Design"))); break;
|
||||
case Tool::Project: m_hdr_project->SetLabel(title(_L_CONTEXT("Project", "Design"))); break;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
@@ -902,8 +903,11 @@ void DesignSketchTool::resolve_live_drag(int dragged_ei, SketchPointRole dragged
|
||||
if (int(r.skipped.size()) != m_skipped_last) {
|
||||
m_skipped_last = int(r.skipped.size());
|
||||
if (m_skipped_last > 0)
|
||||
notify(format(_u8L("%1% constraint(s) cannot act on this geometry (an ellipse or spline curve, "
|
||||
"or a zero-size circle) and are ignored"), m_skipped_last), false);
|
||||
notify(format(into_u8(_L_PLURAL("%1% constraint cannot act on this geometry (an ellipse or spline curve, "
|
||||
"or a zero-size circle) and is ignored",
|
||||
"%1% constraints cannot act on this geometry (an ellipse or spline curve, "
|
||||
"or a zero-size circle) and are ignored", m_skipped_last)),
|
||||
m_skipped_last), false);
|
||||
}
|
||||
// Flag every entity referenced by a conflicting constraint so render() can
|
||||
// tint it red (Onshape/SolveSpace over-constrained feedback), and remember which
|
||||
@@ -8978,7 +8982,8 @@ void DesignSketchTool::confirm_op()
|
||||
|| self_len(d, a) || self_len(d, b);
|
||||
});
|
||||
if (released > 0)
|
||||
notify(format(_u8L("%1% constraint(s) the fillet/chamfer invalidated were removed"), released), false);
|
||||
notify(format(into_u8(_L_PLURAL("%1% constraint the fillet/chamfer invalidated was removed",
|
||||
"%1% constraints the fillet/chamfer invalidated were removed", released)), released), false);
|
||||
auto coin = [&](R xr, int ln, R lr) {
|
||||
SketchEntityConstraintDef d; d.type = CT::Coincident; d.ea = xi; d.ra = xr; d.eb = ln; d.rb = lr; return d; };
|
||||
if (fillet) {
|
||||
@@ -9427,7 +9432,8 @@ void DesignSketchTool::confirm_transform()
|
||||
}
|
||||
});
|
||||
if (released > 0)
|
||||
notify(format(_u8L("%1% constraint(s) released by the transform"), released), false);
|
||||
notify(format(into_u8(_L_PLURAL("%1% constraint released by the transform",
|
||||
"%1% constraints released by the transform", released)), released), false);
|
||||
} else if (m_mode == Mode::Array || m_mode == Mode::PolarArray) {
|
||||
// ADDITIVE: append copies of each subject, then bind each copy to its source. Lines
|
||||
// get Parallel+EqualLength (linear) or EqualLength only (polar — rotation breaks
|
||||
|
||||
@@ -559,7 +559,7 @@ int HistoryWindow::get_extruder_id()
|
||||
void HistoryWindow::on_click_new_button(wxCommandEvent& event)
|
||||
{
|
||||
if (curr_obj && curr_obj->get_printer_series() == PrinterSeries::SERIES_P1P && m_calib_results_history.size() >= 16) {
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle."), 16), wxEmptyString, wxICON_WARNING | wxOK);
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle.", "This machine type can only hold %d historical results per nozzle.", 16), 16), wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -973,7 +973,7 @@ bool PressureAdvanceWizard::can_save_cali_result(const std::vector<PACalibResult
|
||||
}
|
||||
|
||||
if (curr_obj->get_printer_series() != PrinterSeries::SERIES_X1 && curr_obj->pa_calib_tab.size() >= MAX_PA_HISTORY_RESULTS_NUMS) {
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle. This result will not be saved."), MAX_PA_HISTORY_RESULTS_NUMS),
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle. This result will not be saved.", "This machine type can only hold %d historical results per nozzle. This result will not be saved.", MAX_PA_HISTORY_RESULTS_NUMS), MAX_PA_HISTORY_RESULTS_NUMS),
|
||||
wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
return false;
|
||||
|
||||
@@ -352,7 +352,9 @@ void CalibrationCaliPage::update_subtask(MachineObject* obj)
|
||||
prepare_text = wxString::Format(_L("Cloud Slicing..."));
|
||||
}
|
||||
else {
|
||||
prepare_text = wxString::Format(_L("In Cloud Slicing Queue, there are %s tasks ahead of you."), std::to_string(obj->queue_number));
|
||||
prepare_text = wxString::Format(_L_PLURAL("In Cloud Slicing Queue, there is %d task ahead of you.",
|
||||
"In Cloud Slicing Queue, there are %d tasks ahead of you.", obj->queue_number),
|
||||
obj->queue_number);
|
||||
show_percent = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ CaliPageButton::CaliPageButton(wxWindow* parent, CaliPageActionType type, wxStri
|
||||
this->SetLabel(_L("Finish"));
|
||||
break;
|
||||
default:
|
||||
this->SetLabel("Unknown");
|
||||
this->SetLabel(_L("Unknown"));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -644,9 +644,9 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent)
|
||||
|
||||
for (const auto& extruder : curr_obj->GetExtderSystem()->GetExtruders()) {
|
||||
if (extruder.GetNozzleType() == NozzleType::ntUndefine) {
|
||||
wxString name = _L("left");
|
||||
if (extruder.GetExtId() == 0) { name = _L("right"); }
|
||||
wxString msg = wxString::Format(_L("Printer %s nozzle information has not been set. Please configure it before proceeding with the calibration."), name);
|
||||
wxString msg = extruder.GetExtId() == 0 ?
|
||||
_L("Printer right nozzle information has not been set. Please configure it before proceeding with the calibration.") :
|
||||
_L("Printer left nozzle information has not been set. Please configure it before proceeding with the calibration.");
|
||||
MessageDialog msg_dlg(nullptr, msg, wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
break;
|
||||
@@ -2815,7 +2815,7 @@ MaxVolumetricSpeedPresetPage::MaxVolumetricSpeedPresetPage(
|
||||
wxArrayString titles;
|
||||
titles.push_back(_L("From Volumetric Speed"));
|
||||
titles.push_back(_L("To Volumetric Speed"));
|
||||
titles.push_back(_L("Step"));
|
||||
titles.push_back(_L_CONTEXT("Step", "Increment"));
|
||||
m_custom_range_panel->set_titles(titles);
|
||||
|
||||
m_custom_range_panel->set_unit(_L("mm³/s"));
|
||||
|
||||
@@ -680,16 +680,15 @@ void ColorDecomposeDialog::update_filament_limit_warning()
|
||||
return;
|
||||
}
|
||||
|
||||
wxString mode_name;
|
||||
switch (m_selected_mode) {
|
||||
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
|
||||
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
|
||||
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
|
||||
}
|
||||
|
||||
const wxString warning_text = format_wxstr(
|
||||
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
|
||||
m_max_filament_count, mode_name);
|
||||
const unsigned int max_count = static_cast<unsigned int>(m_max_filament_count);
|
||||
const wxString warning_text = m_selected_mode == DecomposeMode::MaterialList ?
|
||||
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
|
||||
"The material list supports at most %1% colors. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
|
||||
max_count), max_count) :
|
||||
// TRN %2% is the decomposition mode name, "CMYW" or "RYBW"
|
||||
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
|
||||
"The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
|
||||
max_count), max_count, m_selected_mode == DecomposeMode::CMYW ? "CMYW" : "RYBW");
|
||||
|
||||
// Show first so the panel is laid out and the text control gets its real
|
||||
// width, then wrap to that width so the paragraph fills the content area.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
@@ -340,8 +341,9 @@ bool prepare_decompose_mixed_result(
|
||||
missing_comp.component_idx = out_result.components.size();
|
||||
missing_comp.official_component = official_component;
|
||||
missing_comp.preset_name = preset_name;
|
||||
missing_comp.display_name = decompose_base_color_display(comp.base_color) +
|
||||
wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type);
|
||||
// TRN %1% is a color name such as "Cyan", %2% is a filament type such as "PLA Basic"
|
||||
missing_comp.display_name = format_wxstr(_L_CONTEXT("%1% %2%", "Color and filament type"),
|
||||
decompose_base_color_display(comp.base_color), wxString::FromUTF8(basic_type));
|
||||
missing.push_back(std::move(missing_comp));
|
||||
out_result.components.push_back(0);
|
||||
}
|
||||
@@ -407,8 +409,11 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
|
||||
missing_text += missing[i].display_name;
|
||||
}
|
||||
|
||||
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
|
||||
"the mixed filament will be created automatically after decomposition."),
|
||||
wxString message = wxString::Format(_L_PLURAL("The current filament list does not contain %s. A project filament required by "
|
||||
"the mixed filament will be created automatically after decomposition.",
|
||||
"The current filament list does not contain %s. Project filaments required by "
|
||||
"the mixed filament will be created automatically after decomposition.",
|
||||
missing.size()),
|
||||
missing_text);
|
||||
|
||||
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
|
||||
|
||||
@@ -35,7 +35,7 @@ CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent)
|
||||
m_list = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
|
||||
wxLC_REPORT | wxSIMPLE_BORDER | wxLC_SINGLE_SEL);
|
||||
m_list->SetMinSize(wxSize(50 * em, 18 * em));
|
||||
m_list->AppendColumn(_L("Model"), wxLIST_FORMAT_LEFT, 8 * em);
|
||||
m_list->AppendColumn(_L_CONTEXT("Model", "Printer model"), wxLIST_FORMAT_LEFT, 8 * em);
|
||||
m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em);
|
||||
m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em);
|
||||
|
||||
@@ -109,7 +109,8 @@ void CrealityDiscoveryDialog::run_discovery()
|
||||
"network and not blocked by Wi-Fi client isolation, then click Scan again."));
|
||||
} else {
|
||||
m_status->SetLabel(wxString::Format(
|
||||
_L("Found %zu Creality printer(s). Select one and click Use Selected."),
|
||||
_L_PLURAL("Found %zu Creality printer. Select it and click Use Selected.",
|
||||
"Found %zu Creality printers. Select one and click Use Selected.", m_rows.size()),
|
||||
m_rows.size()));
|
||||
m_list->Select(0);
|
||||
}
|
||||
|
||||
@@ -4690,7 +4690,7 @@ void EditFilamentPresetDialog::delete_preset()
|
||||
if (count > 0) {
|
||||
msg = _L("Presets inherited by other presets cannot be deleted");
|
||||
msg += "\n";
|
||||
msg += _L_PLURAL("The following presets inherits this preset.", "The following preset inherits this preset.", count);
|
||||
msg += _L_PLURAL("The following preset inherits this preset.", "The following presets inherit this preset.", count);
|
||||
wxString title = _L("Delete Preset");
|
||||
MessageDialog(this, msg + presets, title, wxOK | wxICON_ERROR).ShowModal();
|
||||
m_selected_printer.clear();
|
||||
|
||||
@@ -389,8 +389,8 @@ static void s_update_title(const std::shared_ptr<DevNozzleRack> rack, wgtDeviceN
|
||||
wxString title = _L("Induction Hotend Rack");
|
||||
if (rack && (rack->GetReadingCount() > 0))
|
||||
{
|
||||
wxString pending = ": " + _L("Reading") + wxString::Format(" %d/%d", rack->GetReadingIdx(), rack->GetReadingCount());
|
||||
title += pending;
|
||||
// TRN %d/%d is the hotend being read and the number of hotends to read.
|
||||
title = wxString::Format(_L("Induction Hotend Rack: Reading %d/%d"), rack->GetReadingIdx(), rack->GetReadingCount());
|
||||
}
|
||||
|
||||
title_label->SetLabel(title);
|
||||
|
||||
@@ -385,7 +385,8 @@ void EditGCodeDialog::selection_changed(wxDataViewEvent& evt)
|
||||
// Orca: move below checking for def in custom defined G-code placeholders
|
||||
// This allows custom placeholders to override the default ones for this dialog
|
||||
// Override custom def if selection is within the preset category
|
||||
if (!def || m_params_list->GetSelectedTopLevelCategory() == "Presets") {
|
||||
// The group label is translated, so compare against the translated label.
|
||||
if (!def || m_params_list->GetSelectedTopLevelCategory() == _L("Presets").ToStdString()) {
|
||||
const auto& full_config = wxGetApp().preset_bundle->full_config();
|
||||
if (const ConfigDef* config_def = full_config.def(); config_def && config_def->has(opt_key)) {
|
||||
def = config_def->get(opt_key);
|
||||
|
||||
@@ -124,9 +124,13 @@ void FilamentMapManualPanel::OnTimer(wxTimerEvent &)
|
||||
m_errors->Hide();
|
||||
m_suggestion_panel->Hide();
|
||||
} else {
|
||||
m_errors->SetLabel(wxString::Format(_L("Error: %s extruder has no available %s nozzle, current group result is invalid."),
|
||||
invalid_eid == 0 ? _L_CONTEXT("Left", "Nozzle position") : _L_CONTEXT("Right", "Nozzle position"),
|
||||
invalid_nozzle == NozzleVolumeType::nvtStandard ? _L("Standard") : _L("High Flow")));
|
||||
// [extruder][nozzle volume type]
|
||||
static const char* const error_msgs[2][2] = {
|
||||
{L("Error: Left extruder has no available Standard nozzle, current group result is invalid."),
|
||||
L("Error: Left extruder has no available High Flow nozzle, current group result is invalid.")},
|
||||
{L("Error: Right extruder has no available Standard nozzle, current group result is invalid."),
|
||||
L("Error: Right extruder has no available High Flow nozzle, current group result is invalid.")}};
|
||||
m_errors->SetLabel(_(error_msgs[invalid_eid == 0 ? 0 : 1][invalid_nozzle == NozzleVolumeType::nvtStandard ? 0 : 1]));
|
||||
// Re-wrap: wrapping only applies to the label text present when Wrap is called,
|
||||
// and the label was empty at construction time.
|
||||
m_errors->Wrap(FromDIP(520));
|
||||
|
||||
@@ -3976,17 +3976,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
imgui.text(_u8L("Current grouping of slice result is not optimal."));
|
||||
wxString tip;
|
||||
if (delta_weight_to_best >= 0 && delta_change_to_best >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Increase %1%g filament and %2% changes compared to optimal grouping."))
|
||||
% number_format(delta_weight_to_best)
|
||||
% delta_change_to_best).str());
|
||||
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and %2% change compared to optimal grouping.",
|
||||
"Increase %1%g filament and %2% changes compared to optimal grouping.", delta_change_to_best),
|
||||
number_format(delta_weight_to_best), delta_change_to_best);
|
||||
else if (delta_weight_to_best >= 0 && delta_change_to_best < 0)
|
||||
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to optimal grouping."))
|
||||
% number_format(delta_weight_to_best)
|
||||
% std::abs(delta_change_to_best)).str());
|
||||
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to optimal grouping.",
|
||||
"Increase %1%g filament and save %2% changes compared to optimal grouping.", std::abs(delta_change_to_best)),
|
||||
number_format(delta_weight_to_best), std::abs(delta_change_to_best));
|
||||
else if (delta_weight_to_best < 0 && delta_change_to_best >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to optimal grouping."))
|
||||
% number_format(std::abs(delta_weight_to_best))
|
||||
% delta_change_to_best).str());
|
||||
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to optimal grouping.",
|
||||
"Save %1%g filament and increase %2% changes compared to optimal grouping.", delta_change_to_best),
|
||||
number_format(std::abs(delta_weight_to_best)), delta_change_to_best);
|
||||
|
||||
imgui.text_wrapped(tip, parent_width);
|
||||
ImGui::PopStyleColor(1);
|
||||
@@ -3995,17 +3995,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
wxString tip;
|
||||
if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Save %1%g filament and %2% changes compared to a printer with one nozzle."))
|
||||
% number_format(delta_weight_to_single_ext)
|
||||
% delta_change_to_single_ext).str());
|
||||
tip = format_wxstr(_L_PLURAL("Save %1%g filament and %2% change compared to a printer with one nozzle.",
|
||||
"Save %1%g filament and %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
|
||||
number_format(delta_weight_to_single_ext), delta_change_to_single_ext);
|
||||
else if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext < 0)
|
||||
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to a printer with one nozzle."))
|
||||
% number_format(delta_weight_to_single_ext)
|
||||
% std::abs(delta_change_to_single_ext)).str());
|
||||
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to a printer with one nozzle.",
|
||||
"Save %1%g filament and increase %2% changes compared to a printer with one nozzle.", std::abs(delta_change_to_single_ext)),
|
||||
number_format(delta_weight_to_single_ext), std::abs(delta_change_to_single_ext));
|
||||
else if (delta_weight_to_single_ext < 0 && delta_change_to_single_ext >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to a printer with one nozzle."))
|
||||
% number_format(std::abs(delta_weight_to_single_ext))
|
||||
% delta_change_to_single_ext).str());
|
||||
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to a printer with one nozzle.",
|
||||
"Increase %1%g filament and save %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
|
||||
number_format(std::abs(delta_weight_to_single_ext)), delta_change_to_single_ext);
|
||||
|
||||
imgui.text_wrapped(tip, parent_width);
|
||||
}
|
||||
|
||||
@@ -1639,7 +1639,7 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
|
||||
m_shortcuts_assembly_view = {
|
||||
{_L("Left mouse button"), _L("Object selection")},
|
||||
{alt + _L("Left mouse button"), _L("Part selection")},
|
||||
{"1~16 " + _L("number keys"), _L("Number keys can quickly change the color of objects")},
|
||||
{_L("1~16 number keys"), _L("Number keys can quickly change the color of objects")},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3053,10 +3053,9 @@ void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z)
|
||||
InstancesToZMap instances_min_z;
|
||||
|
||||
for (GLVolume* volume : m_volumes.volumes) {
|
||||
ModelObject* mo = m_model->objects[volume->object_idx()];
|
||||
ModelInstance* mi = mo->instances[volume->instance_idx()];
|
||||
ModelObject* mo = m_model->objects[volume->object_idx()];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5604,9 +5603,8 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5735,9 +5733,8 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int> &i : done) {
|
||||
ModelObject *mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5838,9 +5835,8 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5959,9 +5955,8 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -11195,7 +11190,7 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f * f_scale);
|
||||
imgui.tooltip(_L("Shortcut Key ") + std::to_string(i + 1), ImGui::GetFontSize() * 20.0f);
|
||||
imgui.tooltip(wxString::Format(_L("Shortcut Key %d"), i + 1), ImGui::GetFontSize() * 20.0f);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
}
|
||||
@@ -12086,12 +12081,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
extruder_name += (boost::format(_u8L("Tool %d"))%extruder_id).str();
|
||||
}
|
||||
|
||||
if (error_iter->second.size() == 1) {
|
||||
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
|
||||
}
|
||||
else {
|
||||
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
|
||||
}
|
||||
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
|
||||
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
|
||||
error_iter->second.size()))) %filaments %extruder_name %extruder_name).str();
|
||||
}
|
||||
error = ErrorType::SLICING_LIMIT_ERROR;
|
||||
break;
|
||||
@@ -12139,11 +12131,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
}
|
||||
}
|
||||
std::string extruder_name = extruder_name_list[extruder_id-1];
|
||||
if (error_iter->second.size() == 1) {
|
||||
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
|
||||
} else {
|
||||
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
|
||||
}
|
||||
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
|
||||
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
|
||||
filament_ids.size()))) % filaments % extruder_name % extruder_name).str();
|
||||
}
|
||||
if (!text.empty()) {
|
||||
text += "\n";
|
||||
@@ -12170,7 +12160,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
warning += std::to_string(filament + 1);
|
||||
warning += " ";
|
||||
}
|
||||
text = (boost::format(_u8L("Filaments %s cannot be printed directly on the surface of this plate.")) % warning).str();
|
||||
text = (boost::format(into_u8(_L_PLURAL("Filament %s cannot be printed directly on the surface of this plate.",
|
||||
"Filaments %s cannot be printed directly on the surface of this plate.", conflict_filament.size()))) % warning).str();
|
||||
error = ErrorType::SLICING_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1109,6 +1109,8 @@ void GUI_App::post_init()
|
||||
}
|
||||
|
||||
this->check_new_version_sf();
|
||||
if (!cw_showed)
|
||||
this->check_legacy_user_filament_presets(false);
|
||||
const auto cloud_provider = get_printer_cloud_provider();
|
||||
if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) {
|
||||
// this->check_privacy_version(0);
|
||||
@@ -3549,6 +3551,10 @@ bool GUI_App::on_init_inner()
|
||||
}
|
||||
|
||||
// BBS if load user preset failed
|
||||
// Let the libslic3r know the callback, which will translate messages on demand.
|
||||
// Installed before the presets load so that their load errors are translated too.
|
||||
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
|
||||
|
||||
//if (loaded_preset_result != 0) {
|
||||
try {
|
||||
// Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only.
|
||||
@@ -3566,9 +3572,6 @@ bool GUI_App::on_init_inner()
|
||||
register_win32_device_notification_event();
|
||||
#endif // WIN32
|
||||
|
||||
// Let the libslic3r know the callback, which will translate messages on demand.
|
||||
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
|
||||
|
||||
#if defined(__WXGTK__) && wxHAS_EGL
|
||||
// Configure GL backend before any wxGLCanvas is created.
|
||||
// On X11, prefer GLX for maximum driver compatibility.
|
||||
@@ -6811,6 +6814,71 @@ void GUI_App::reload_settings()
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::check_legacy_user_filament_presets(bool by_user)
|
||||
{
|
||||
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
|
||||
return;
|
||||
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
|
||||
if (legacy_presets.empty()) {
|
||||
if (by_user)
|
||||
MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."),
|
||||
_L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal();
|
||||
return;
|
||||
}
|
||||
|
||||
wxString message = _L("OrcaSlicer 2.5.0 restructured many system filament presets to fix incorrect profiles and prepare "
|
||||
"for future features. As a result, the presets below no longer show on some printers they were made "
|
||||
"for. Sorry for the trouble; this is a one-time fix.\n\n"
|
||||
"Add those printers back to your presets now? Your presets are backed up first. You can also do this "
|
||||
"later from Help > Migrate Filament Presets.") + "\n";
|
||||
for (const LegacyUserFilamentPreset &legacy : legacy_presets) {
|
||||
message += "\n" + format_wxstr(_L("%1% (based on %2%)"), legacy.preset, legacy.parent);
|
||||
for (const std::string &printer : legacy.printers)
|
||||
message += "\n " + from_u8(printer);
|
||||
}
|
||||
RichMessageDialog dlg(mainframe, message, _L("Migrate Filament Presets"), wxYES | wxNO | wxICON_QUESTION);
|
||||
if (!by_user)
|
||||
dlg.ShowCheckBox(_L("Don't remind me again"));
|
||||
const bool migrate = dlg.ShowModal() == wxID_YES;
|
||||
if (dlg.IsCheckBoxChecked())
|
||||
app_config->set_bool("legacy_user_filament_presets_dont_remind", true);
|
||||
if (!migrate)
|
||||
return;
|
||||
|
||||
const PresetBundle::LegacyUserFilamentPresetMigration migration = preset_bundle->migrate_legacy_user_filament_presets(legacy_presets);
|
||||
const std::string &backup = migration.backup;
|
||||
const std::vector<std::string> &failed = migration.failed;
|
||||
if (backup.empty()) {
|
||||
show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated."));
|
||||
return;
|
||||
}
|
||||
if (!migration.locked) {
|
||||
// The lock is also refused for a while after a miss, and when the lock file itself cannot be opened.
|
||||
show_error(mainframe, _L("The user presets folder could not be locked, so nothing was migrated. Another OrcaSlicer "
|
||||
"instance may be saving presets; try again later from Help > Migrate Filament Presets."));
|
||||
return;
|
||||
}
|
||||
if (failed.size() == legacy_presets.size()) {
|
||||
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
|
||||
return;
|
||||
}
|
||||
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
|
||||
mainframe->update_side_preset_ui();
|
||||
for (auto tab : tabs_list) {
|
||||
tab->reload_config();
|
||||
tab->update_changed_ui();
|
||||
}
|
||||
|
||||
message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n"
|
||||
"A backup of your presets was saved to:\n%1%"), backup);
|
||||
if (!failed.empty()) {
|
||||
message += "\n\n" + _L("These presets could not be migrated:");
|
||||
for (const std::string &name : failed)
|
||||
message += "\n" + from_u8(name);
|
||||
}
|
||||
MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal();
|
||||
}
|
||||
|
||||
//BBS reload when logout
|
||||
void GUI_App::remove_user_presets()
|
||||
{
|
||||
@@ -8577,22 +8645,23 @@ void GUI_App::refresh_plugins()
|
||||
// and report completion through the notification manager -- the speed dial needs no dialog.
|
||||
std::thread([]() {
|
||||
wxString error;
|
||||
bool unknown_error = false;
|
||||
try {
|
||||
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
|
||||
} catch (const std::exception& ex) {
|
||||
error = from_u8(ex.what());
|
||||
} catch (...) {
|
||||
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
|
||||
unknown_error = true; // translated in CallAfter: wx translation isn't safe off the UI thread
|
||||
}
|
||||
if (!wxTheApp)
|
||||
return;
|
||||
wxTheApp->CallAfter([error]() {
|
||||
wxTheApp->CallAfter([error, unknown_error]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
if (error.IsEmpty())
|
||||
if (error.IsEmpty() && !unknown_error)
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
@@ -8601,7 +8670,7 @@ void GUI_App::refresh_plugins()
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
||||
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
|
||||
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), unknown_error ? _L("Unknown error") : error)));
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user