mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-14 11:21:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
229460595f | ||
|
|
46b3871f8d | ||
|
|
84768da6a1 | ||
|
|
bb0d8ee1c4 | ||
|
|
f32591ed64 | ||
|
|
db9917e722 | ||
|
|
cb752d38a3 | ||
|
|
5bce937d07 | ||
|
|
de3af0b7de | ||
|
|
5a427f2eb6 | ||
|
|
008ee792f6 | ||
|
|
1591423f91 | ||
|
|
7131dd30e7 | ||
|
|
84feeaa587 | ||
|
|
5c8a287b5e | ||
|
|
a0a4a33c72 | ||
|
|
02b0d2b614 | ||
|
|
0dd4b98604 | ||
|
|
a338f6ca7d | ||
|
|
e821665e4b | ||
|
|
b77b5afd49 | ||
|
|
f546551b3f | ||
|
|
ab42ddeef2 | ||
|
|
39c77325ce | ||
|
|
af29e4d553 | ||
|
|
df392508b7 | ||
|
|
11493a2236 | ||
|
|
df4c8cd531 | ||
|
|
bf5a378479 | ||
|
|
01499e536e | ||
|
|
f33a9bf999 | ||
|
|
869e450981 | ||
|
|
b920116fbd | ||
|
|
237586675f | ||
|
|
6d35ff6b9a | ||
|
|
075b03975f | ||
|
|
a379ec4a67 | ||
|
|
6700d4c5df | ||
|
|
a28c9c3a82 | ||
|
|
3dd547c8be | ||
|
|
f0e9cdc470 | ||
|
|
58a71331d0 | ||
|
|
f127b758ed | ||
|
|
1494de66de | ||
|
|
3ca182a98e | ||
|
|
5e19fc0e6c | ||
|
|
3f4709aaa7 | ||
|
|
c63bd5d474 | ||
|
|
6263502697 | ||
|
|
1a9201690d | ||
|
|
c6a338f35f | ||
|
|
4f2d2ae11a | ||
|
|
c57b5dc6c5 | ||
|
|
7149728219 | ||
|
|
f49d49b5a6 | ||
|
|
08f2037cbd | ||
|
|
1abd6649c7 | ||
|
|
d8cae3268a | ||
|
|
7d4ec3494c | ||
|
|
cccf53850e | ||
|
|
5095733323 | ||
|
|
0cec10748e | ||
|
|
3f78959a0c | ||
|
|
30e48183e3 | ||
|
|
56b559d36a | ||
|
|
ed1f3e52c9 | ||
|
|
8ecfee6a21 | ||
|
|
fbf4652e12 | ||
|
|
be9ff50130 | ||
|
|
dee1f1715f | ||
|
|
fcb1f62bd9 | ||
|
|
bea412fddf | ||
|
|
391c53a8da |
@@ -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()
|
||||
@@ -570,6 +570,11 @@ Details [source] (`Widgets/PopupWindow.cpp`).
|
||||
- On GTK the sub binds an idle handler. While both are shown and the cursor is over main but not over the
|
||||
sub, it builds a `wxMouseEvent(wxEVT_MOTION)` from `wxGetMousePosition()`, calls `mainDropDown->mouseMove(ev)`
|
||||
**directly** (nothing is posted), and calls `e.RequestMore()` to keep polling.
|
||||
- On GTK the sub also receives the wheel events the user raises over main: GDK sends a `wl_pointer.axis` only
|
||||
to the surface the pointer is over, and GTK then hands that `GDK_SCROLL` to the chain's grab widget, which is
|
||||
the sub. The sub installs a `GtkWidget::event` handler that replays the event on main with
|
||||
`gtk_propagate_event(main->GetConnectWidget(), event)` when the event's GDK window belongs to main
|
||||
(`DropDown::messureSize`).
|
||||
- **GTK wrapper sizing.** `messureSize` calls `gtk_window_resize` on `m_widget` after `SetSize`, because wxGTK's
|
||||
`DoSetSize` only uses `gtk_widget_set_size_request` ("gtk_window_resize does not work for GTK_WINDOW_POPUP",
|
||||
`src/gtk/popupwin.cpp:181-188`).
|
||||
@@ -607,6 +612,32 @@ Details [source] (`Widgets/PopupWindow.cpp`).
|
||||
```
|
||||
Cite: c0ae2bda99, b9ff15054f (`DropDown::mouseMove`, `DropDown::messureSize`, `DropDown::ShouldDismissOnTopWindowDeactivate`,
|
||||
`PopupWindow::ShouldDismissOnTopWindowDeactivate`).
|
||||
- **Rule:** On GTK, route a chained popup's wheel event by the GDK window it names, not by the grab widget and not
|
||||
by the global cursor position.
|
||||
**Why:** While the sub holds the grab, `gtk_main_do_event` hands every `GDK_SCROLL` of the chain to the grab
|
||||
widget (the sub) with its original, un-rebased coordinates [source] (`gtkmain.c:1789-1792`, 1923-1924), while
|
||||
GDK Wayland sends the axis to the surface under the pointer only (`pointer_handle_axis` → `create_scroll_event`,
|
||||
`gdkdevice-wayland.c:1929, 1527`). The sub therefore sees and consumes wheel events raised over main, so the
|
||||
main list never scrolls, on X11 and Wayland alike. A hit test against `wxGetMousePosition()` cannot replace the
|
||||
window test: global coordinates are unreliable on Wayland (`mouse-keyboard-focus.md`). `DropDown` connects a
|
||||
`GtkWidget::event` handler on the sub's `GtkWindow` (its `m_widget`), which is emitted before wx's
|
||||
`::scroll-event` handler on the same widget and suppresses it when it returns TRUE (`gtkwidget.c:7696`), then
|
||||
delivers the event with `gtk_propagate_event(main->GetConnectWidget(), event)`. Returning TRUE there also stops
|
||||
the parent-chain propagation.
|
||||
```cpp
|
||||
// Wrong: emitted on the popup's outer GtkWindow, where wx has no handlers at all
|
||||
g_signal_emit_by_name(main->m_widget, "scroll-event", event, &handled);
|
||||
// Right (wxGTK): replay GTK's own delivery on the widget wx listens to
|
||||
gtk_propagate_event(main->GetConnectWidget(), event);
|
||||
```
|
||||
**Where wx listens [source].** `wxPopupWindow::Create` builds a popup from two widgets: `m_widget` is the outer
|
||||
`GtkWindow` and `m_wxwindow` is a `wxPizza` child added to it. `wxWindowGTK::GetConnectWidget()` is
|
||||
`m_wxwindow ? m_wxwindow : m_widget`, and `ConnectWidget()` — which binds `button_press_event`, `motion_notify_event`
|
||||
and `scroll_event` — is called on that widget (`src/gtk/window.cpp` `Create`, `GetConnectWidget`). A signal emitted
|
||||
on the outer `GtkWindow` therefore reaches no wx handler; only events GTK delivers to the inner widget, or to a
|
||||
descendant of the grab widget, are seen by wx. `GTKGetConnectWindow()`/`GTKGetMainWindow()` are the matching
|
||||
GdkWindows.
|
||||
Cite: `DropDown::messureSize`, `DropDown::mouseWheelMoved`.
|
||||
- **Rule:** When shrinking a dropdown to fit above the display bottom, give up if fewer than two rows would
|
||||
remain.
|
||||
**Why:** On Wayland the popup can land where almost no vertical space is left. Clamping to that sliver made an
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: 'Deploy Nightly'
|
||||
description: 'Upload a file to a release, replacing the asset of the same name'
|
||||
inputs:
|
||||
asset_path:
|
||||
description: 'Path of the file to upload'
|
||||
required: true
|
||||
asset_name:
|
||||
description: 'Asset name on the release'
|
||||
required: true
|
||||
content_type:
|
||||
description: 'Content type of the uploaded file'
|
||||
required: false
|
||||
default: 'application/octet-stream'
|
||||
tag:
|
||||
description: 'Tag of the release to upload to'
|
||||
required: false
|
||||
default: 'nightly-builds'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Replace release asset
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ASSET_PATH: ${{ inputs.asset_path }}
|
||||
ASSET_NAME: ${{ inputs.asset_name }}
|
||||
CONTENT_TYPE: ${{ inputs.content_type }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
run: bash "$GITHUB_ACTION_PATH/deploy.sh"
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# Replaces one asset of a GitHub release, uploading under a temporary name
|
||||
# first so a failed upload leaves the old asset in place.
|
||||
#
|
||||
# Environment: GH_TOKEN, ASSET_PATH, ASSET_NAME, CONTENT_TYPE, RELEASE_TAG and
|
||||
# the GITHUB_* variables Actions sets.
|
||||
|
||||
# The $names inside the single-quoted jq programs are jq variables.
|
||||
# shellcheck disable=SC2016
|
||||
set -euo pipefail
|
||||
|
||||
repo_api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY"
|
||||
|
||||
if [ ! -f "$ASSET_PATH" ]; then
|
||||
echo "::error::$ASSET_PATH does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The speed limit turns a stalled transfer into an error that --retry handles.
|
||||
curl_opts=(-sS --retry 3 --connect-timeout 30 --speed-limit 1024 --speed-time 60
|
||||
-H "Authorization: Bearer $GH_TOKEN")
|
||||
|
||||
# Prints the response body, or reports GitHub's error body and fails.
|
||||
api() {
|
||||
local out
|
||||
if ! out=$(curl "${curl_opts[@]}" --fail-with-body "$@"); then
|
||||
# A workflow command ends at the first newline.
|
||||
echo "::error::GitHub API request failed: ${out//[$'\r\n']/ }" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
http_status() {
|
||||
curl "${curl_opts[@]}" -o /dev/null -w '%{http_code}' "$@"
|
||||
}
|
||||
|
||||
# jq prints CRLF on Windows.
|
||||
jqr() {
|
||||
jq -r "$@" | tr -d '\r'
|
||||
}
|
||||
|
||||
delete_asset() {
|
||||
local code
|
||||
code=$(http_status -X DELETE "$repo_api/releases/assets/$1")
|
||||
case $code in
|
||||
# A 404 means a concurrent run already deleted the asset.
|
||||
204 | 404) ;;
|
||||
*)
|
||||
echo "::error::Deleting release asset $1 failed with HTTP $code" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
list_assets() {
|
||||
local page=1 json
|
||||
while :; do
|
||||
# Callers run this inside $(...), where bash ignores set -e.
|
||||
json=$(api "$repo_api/releases/$release_id/assets?per_page=100&page=$page") || exit 1
|
||||
printf '%s\n' "$json"
|
||||
[ "$(jqr length <<<"$json")" -eq 100 ] || break
|
||||
page=$((page + 1))
|
||||
done | jq -s add
|
||||
}
|
||||
|
||||
release=$(api "$repo_api/releases/tags/$RELEASE_TAG")
|
||||
release_id=$(jqr .id <<<"$release")
|
||||
upload_url=$(jqr .upload_url <<<"$release")
|
||||
upload_url=${upload_url%%\{*}
|
||||
|
||||
# Unique per run attempt, so concurrent runs never upload to the same name.
|
||||
tmp_name="$ASSET_NAME.partial-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
||||
|
||||
# Deletes leftover partials older than an hour, an age no upload in progress reaches.
|
||||
stale_ids=$(list_assets | jqr --arg prefix "$ASSET_NAME.partial-" \
|
||||
'.[] | select((.name | startswith($prefix)) and (.created_at | fromdateiso8601) < now - 3600) | .id')
|
||||
for id in $stale_ids; do
|
||||
delete_asset "$id"
|
||||
done
|
||||
|
||||
name_query=$(jqr -n --arg name "$tmp_name" '$name | @uri')
|
||||
# Uploads once, since a retry after an upload that succeeded fails on the taken name.
|
||||
new_id=$(api --retry 0 -X POST -H "Content-Type: $CONTENT_TYPE" -T "$ASSET_PATH" \
|
||||
"$upload_url?name=$name_query" | jqr .id)
|
||||
trap 'delete_asset "$new_id" || true' EXIT
|
||||
|
||||
rename_body=$(jq -n --arg name "$ASSET_NAME" '{name: $name}')
|
||||
# Retries on HTTP 422, which a concurrent run causes by renaming its own upload
|
||||
# between this delete and this rename.
|
||||
for _ in 1 2 3; do
|
||||
old_ids=$(list_assets | jqr --arg name "$ASSET_NAME" '.[] | select(.name == $name) | .id')
|
||||
for id in $old_ids; do
|
||||
delete_asset "$id"
|
||||
done
|
||||
code=$(http_status -X PATCH -d "$rename_body" "$repo_api/releases/assets/$new_id")
|
||||
if [ "$code" = 200 ]; then
|
||||
trap - EXIT
|
||||
exit 0
|
||||
fi
|
||||
[ "$code" = 422 ] || break
|
||||
done
|
||||
echo "::error::Renaming $tmp_name to $ASSET_NAME failed with HTTP $code" >&2
|
||||
exit 1
|
||||
@@ -441,14 +441,10 @@ jobs:
|
||||
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
|
||||
- name: Deploy Flatpak to nightly release
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
|
||||
asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
|
||||
# The asset is /app (the exes link it at runtime) plus the build tree
|
||||
# slimmed to what ctest needs.
|
||||
- name: Package flatpak test asset
|
||||
|
||||
@@ -341,12 +341,12 @@ jobs:
|
||||
|
||||
# Notarize main DMG
|
||||
xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
|
||||
xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
|
||||
retry xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
|
||||
|
||||
# Notarize profile validator DMG if it exists
|
||||
if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then
|
||||
xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
|
||||
xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
|
||||
retry xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
|
||||
fi
|
||||
|
||||
- name: Create DMG without notary
|
||||
@@ -397,25 +397,18 @@ jobs:
|
||||
|
||||
- name: Deploy Mac release
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
|
||||
asset_name: OrcaSlicer_Mac_universal_nightly.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
|
||||
|
||||
# The DMG is only built when the validator app was.
|
||||
- name: Deploy Mac OrcaSlicer_profile_validator DMG release
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED && hashFiles(format('OrcaSlicer_profile_validator_Mac_universal_{0}.dmg', env.ver)) != ''
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ${{ github.workspace }}/OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
|
||||
asset_name: OrcaSlicer_profile_validator_Mac_universal_nightly.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
# Windows
|
||||
- name: Set Windows build variables
|
||||
@@ -460,10 +453,6 @@ jobs:
|
||||
- name: Build slicer Win
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: ${{ github.workspace }}
|
||||
# Orca: Removed Netfabb STL fixing service support in favor of CGAL.
|
||||
# env:
|
||||
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
|
||||
# WindowsSDKVersion: '10.0.26100.0\'
|
||||
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
|
||||
run: |
|
||||
# cache-path is the install directory inside the deps build directory.
|
||||
@@ -546,36 +535,27 @@ jobs:
|
||||
|
||||
- name: Deploy Windows release portable
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
|
||||
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip
|
||||
asset_content_type: application/x-zip-compressed
|
||||
max_releases: 1
|
||||
content_type: application/x-zip-compressed
|
||||
|
||||
- name: Deploy Windows release installer
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
|
||||
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe
|
||||
asset_content_type: application/x-msdownload
|
||||
max_releases: 1
|
||||
content_type: application/x-msdownload
|
||||
|
||||
- name: Deploy Windows OrcaSlicer_profile_validator release
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
|
||||
asset_name: OrcaSlicer_profile_validator_Windows_nightly.exe
|
||||
asset_content_type: application/x-msdownload
|
||||
max_releases: 1
|
||||
content_type: application/x-msdownload
|
||||
|
||||
- name: Build MSIX Store package Win
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
@@ -704,14 +684,10 @@ jobs:
|
||||
|
||||
- name: Deploy Ubuntu release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
|
||||
asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly.AppImage
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
|
||||
- name: Deploy Ubuntu release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
run: |
|
||||
@@ -720,36 +696,24 @@ jobs:
|
||||
|
||||
- name: Deploy Ubuntu OrcaSlicer_profile_validator release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ./build/src/Release/OrcaSlicer_profile_validator
|
||||
asset_name: OrcaSlicer_profile_validator_Linux${{ env.ubuntu-ver-str }}_nightly
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy Ubuntu generate_system_cache release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ./build/src/dev-utils/Release/generate_system_cache
|
||||
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy orca_custom_preset_tests
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ${{ github.workspace }}/resources/profiles/orca_custom_preset_tests.zip
|
||||
asset_name: orca_custom_preset_tests.zip
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
# The build has just touched everything it can use, so an object
|
||||
# untouched for a week is dead, usually orphaned by a flag change.
|
||||
|
||||
@@ -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).'
|
||||
|
||||
@@ -16,6 +16,15 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Listing caches and builds while waiting for a deps cache.
|
||||
actions: read
|
||||
|
||||
# Read by scripts/ci_deps_cache.sh.
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
WORKFLOW: build_all.yml
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
JOB: build_linux (${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }})
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
@@ -35,7 +44,7 @@ jobs:
|
||||
# The PR merge commit plus its first parent, the base it is diffed against.
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Look for changed C++ files
|
||||
- name: Look for changed files
|
||||
id: changes
|
||||
run: |
|
||||
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
|
||||
@@ -43,18 +52,75 @@ jobs:
|
||||
else
|
||||
echo "No C++ changes under src/ or tests/."
|
||||
fi
|
||||
if ! git diff --quiet HEAD^1 -- deps; then
|
||||
echo "deps=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "deps-key=linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Parsing needs the dependency headers, not a build of this PR's deps/, so
|
||||
# a PR that changes deps/ falls back to the newest cache main has.
|
||||
# Parsing needs the dependency headers, not a build of this PR's deps/.
|
||||
# Without an exact match the job waits for the build that saves it: this
|
||||
# PR's own when it changes deps/, otherwise a running push build of the
|
||||
# base branch with the same deps. With neither, a PR that keeps deps/
|
||||
# falls back to the newest cache.
|
||||
- name: Restore cached deps
|
||||
id: deps_cache
|
||||
if: steps.changes.outputs.cpp == 'true'
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
|
||||
key: ${{ steps.changes.outputs.deps-key }}
|
||||
|
||||
- name: Look for a build to wait for
|
||||
id: wait_check
|
||||
if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }}
|
||||
|
||||
# Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The
|
||||
# wait ends early when the deps job fails.
|
||||
- name: Wait for the deps cache
|
||||
if: steps.wait_check.outputs.wait-for != ''
|
||||
timeout-minutes: 100
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
KEY: ${{ steps.changes.outputs.deps-key }}
|
||||
CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }}
|
||||
KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }}
|
||||
WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }}
|
||||
WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }}
|
||||
WAIT_MINUTES: 90
|
||||
run: bash scripts/ci_deps_cache.sh wait
|
||||
|
||||
- name: Restore the deps cache after waiting
|
||||
id: waited_cache
|
||||
if: steps.wait_check.outputs.wait-for != ''
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||
key: ${{ steps.changes.outputs.deps-key }}
|
||||
|
||||
- name: Fail without this PR's deps cache
|
||||
if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy."
|
||||
exit 1
|
||||
|
||||
- name: Restore the newest cached deps
|
||||
id: newest_cache
|
||||
if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||
key: ${{ steps.changes.outputs.deps-key }}
|
||||
restore-keys: linux-clang-cache-orcaslicer_deps-build-
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Warn about older deps
|
||||
if: steps.newest_cache.outcome == 'success' && steps.newest_cache.outputs.cache-hit != 'true'
|
||||
run: echo "::warning title=Older deps::No cache matches this PR's deps/, so clang-tidy parsed against ${{ steps.newest_cache.outputs.cache-matched-key }}. If it reports errors in code that uses a dependency, rerun clang-tidy once the base branch's Build all has saved the new deps."
|
||||
|
||||
- name: Apt-Install Dependencies
|
||||
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
|
||||
uses: ./.github/actions/apt-install-deps
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
name: clang-tidy fix
|
||||
|
||||
# Makes the missing-include fix that /bot fix-tidy (pr-tidy-bot.yml) posts on a
|
||||
# pull request: scripts/run_clang_tidy.sh --fix on the merge commit the
|
||||
# clang-tidy check runs on, uploaded as the clang-tidy-fix artifact.
|
||||
#
|
||||
# Every push to a PR starts this workflow, and its job skips on that first
|
||||
# attempt. The bot re-runs it on request. A re-run keeps the pull_request
|
||||
# context, so the PR's code runs with a read-only token, no secrets and the
|
||||
# PR's own cache, never main's, whoever asked for it.
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fix:
|
||||
if: github.run_attempt > 1
|
||||
# A hosted runner, so nothing the PR's code leaves behind outlives the job.
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: 'false'
|
||||
# The PR merge commit plus its first parent, the base it is diffed against.
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
# Parsing needs the dependency headers, not a build of this PR's deps/, so
|
||||
# a PR that changes deps/ falls back to the newest cache main has.
|
||||
- name: Restore cached deps
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
|
||||
restore-keys: linux-clang-cache-orcaslicer_deps-build-
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Apt-Install Dependencies
|
||||
uses: ./.github/actions/apt-install-deps
|
||||
|
||||
# The status says whether findings remain that --fix cannot fix.
|
||||
- name: Fix the changed lines
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/clang-tidy-fix"
|
||||
status=0
|
||||
scripts/run_clang_tidy.sh --base HEAD^1 --yes --fix || status=$?
|
||||
echo "$status" > "$RUNNER_TEMP/clang-tidy-fix/status"
|
||||
git -c core.quotePath=false diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ \
|
||||
> "$RUNNER_TEMP/clang-tidy-fix/fix.patch"
|
||||
|
||||
- name: Upload the fix
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: clang-tidy-fix
|
||||
path: ${{ runner.temp }}/clang-tidy-fix/
|
||||
retention-days: 7
|
||||
# A later re-run of the same run replaces the earlier fix.
|
||||
overwrite: true
|
||||
@@ -1,476 +0,0 @@
|
||||
name: PR clang-tidy Bot
|
||||
|
||||
# Helps a pull request past the clang-tidy check.
|
||||
#
|
||||
# When the check fails on a PR's changes, a comment asks the author to fix it,
|
||||
# either with scripts/run_clang_tidy.sh --fix or by commenting:
|
||||
# /bot fix-tidy
|
||||
#
|
||||
# That command, from the PR's author or an account with write access, re-runs
|
||||
# the PR's clang-tidy fix run (clang_tidy_fix.yml), which makes the fix in the
|
||||
# PR's own context. When it finishes, the fix is posted as one review: each
|
||||
# include whose place falls on a line the PR's diff shows is a suggestion GitHub
|
||||
# can commit in one click, and the review's body holds the whole patch with a
|
||||
# git apply command. Findings --fix cannot fix, such as a using in a header,
|
||||
# are left for the check to report.
|
||||
#
|
||||
# No code from the PR runs here. Nothing is pushed, and the patch is only
|
||||
# posted when every line it adds is an #include in a C++ file the PR changes.
|
||||
#
|
||||
# workflow_run matches the workflows by their name: keep "clang-tidy" and
|
||||
# "clang-tidy fix" in step with clang_tidy.yml and clang_tidy_fix.yml, and
|
||||
# TIDY_STEP below with the clang-tidy step's name in clang_tidy.yml.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
workflow_run:
|
||||
workflows:
|
||||
- clang-tidy
|
||||
- clang-tidy fix
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
request:
|
||||
# Skips the job unless a PR comment mentions the command.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'issue_comment'
|
||||
&& github.event.issue.pull_request != null
|
||||
&& contains(github.event.comment.body, '/bot fix-tidy')
|
||||
permissions:
|
||||
actions: write # re-run the fix run
|
||||
pull-requests: write # react to and answer the comment, which is the PR's
|
||||
issues: write # feedback comment + reactions
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Re-run the PR's clang-tidy fix
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const MARKER = '<!-- pr-tidy-bot -->';
|
||||
const FIX_WORKFLOW = 'clang_tidy_fix.yml';
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const issue = context.payload.issue;
|
||||
const comment = context.payload.comment;
|
||||
|
||||
// Ignores a comment whose sender is not its author.
|
||||
if (context.payload.sender.login !== comment.user.login || comment.user.type !== 'User') {
|
||||
core.info('Ignoring a comment not written by its sender, or by a bot.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commandLine = (comment.body || '')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => /^\/bot\s+fix-tidy\b/i.test(line));
|
||||
if (!commandLine) {
|
||||
core.info('No /bot fix-tidy command found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commenter = comment.user.login;
|
||||
// Answers only the PR's author and maintainers, so the command cannot be used to spam.
|
||||
if (commenter !== issue.user.login) {
|
||||
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: commenter });
|
||||
if (!['admin', 'write'].includes(access.permission)) {
|
||||
core.info(`Ignoring /bot fix-tidy from @${commenter}: not the PR's author and no write access.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const react = (content) =>
|
||||
github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content });
|
||||
async function refuse(reason) {
|
||||
await react('-1');
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n@${commenter} ${reason}` });
|
||||
}
|
||||
|
||||
await react('eyes');
|
||||
|
||||
if (!/^\/bot\s+fix-tidy$/i.test(commandLine)) {
|
||||
return refuse('`/bot fix-tidy` takes no arguments.');
|
||||
}
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue.number });
|
||||
if (pr.state !== 'open') {
|
||||
return refuse('the PR is not open.');
|
||||
}
|
||||
|
||||
const sha = pr.head.sha.slice(0, 10);
|
||||
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
|
||||
owner, repo, workflow_id: FIX_WORKFLOW, event: 'pull_request', head_sha: pr.head.sha, per_page: 1
|
||||
});
|
||||
const run = runs[0];
|
||||
// A PR last pushed before clang_tidy_fix.yml existed has no run to re-run.
|
||||
if (!run) {
|
||||
return refuse(`there is no clang-tidy fix run for ${sha}, the PR's latest commit. If you just pushed it, ` +
|
||||
'try again in a minute. Otherwise, push a commit or merge the base branch to start one, then comment `/bot fix-tidy` again.');
|
||||
}
|
||||
if (run.status !== 'completed') {
|
||||
// The first attempt is the one every push starts, and it makes no fix.
|
||||
return refuse(run.run_attempt > 1
|
||||
? `a clang-tidy fix for ${sha} is already [running](${run.html_url}). I'll post it when it finishes.`
|
||||
: `the PR's checks on ${sha} are still starting. Comment \`/bot fix-tidy\` again in a minute.`);
|
||||
}
|
||||
// GitHub refuses to re-run a run created over 30 days ago.
|
||||
try {
|
||||
await github.rest.actions.reRunWorkflow({ owner, repo, run_id: run.id });
|
||||
} catch (error) {
|
||||
core.warning(`Re-running ${run.html_url} failed: ${error.message}`);
|
||||
return refuse(`GitHub won't re-run the [clang-tidy fix run](${run.html_url}) for ${sha} (${error.message}). ` +
|
||||
'Push a commit or merge the base branch to start a new one, then comment `/bot fix-tidy` again.');
|
||||
}
|
||||
core.info(`Re-ran ${run.html_url}`);
|
||||
|
||||
suggest:
|
||||
# The fix run's re-runs only. Its first attempt makes no fix.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'workflow_run'
|
||||
&& github.event.workflow_run.name == 'clang-tidy fix'
|
||||
&& github.event.workflow_run.event == 'pull_request'
|
||||
&& github.event.workflow_run.run_attempt > 1
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-suggest-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
actions: read # download the fix
|
||||
contents: read # check out the PR head
|
||||
pull-requests: write # the review
|
||||
issues: write # feedback comment + reactions
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Find the PR and who asked
|
||||
id: find
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const MARKER = '<!-- pr-tidy-bot -->';
|
||||
const CPP = /^(src|tests)\/.+\.(cpp|cc|cxx|hpp|h|hxx)$/;
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const run = context.payload.workflow_run;
|
||||
|
||||
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||
});
|
||||
const pr = prs[0];
|
||||
if (!pr) {
|
||||
core.info('No open PR for this run.');
|
||||
return;
|
||||
}
|
||||
|
||||
// The latest /bot fix-tidy comment is the one this run answers.
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||
const request = comments.reverse().find((c) => c.user.type === 'User' && /^\s*\/bot\s+fix-tidy\s*$/im.test(c.body || ''));
|
||||
const mention = request ? `@${request.user.login} ` : '';
|
||||
async function refuse(reason) {
|
||||
if (request) {
|
||||
await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: request.id, content: '-1' });
|
||||
}
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: `${MARKER}\n${mention}${reason}` });
|
||||
}
|
||||
|
||||
if (pr.head.sha !== run.head_sha) {
|
||||
return refuse('the PR changed while I was making its clang-tidy fix. Comment `/bot fix-tidy` again.');
|
||||
}
|
||||
if (run.conclusion !== 'success') {
|
||||
return refuse(`I couldn't make the clang-tidy fix. See the [run log](${run.html_url}).`);
|
||||
}
|
||||
|
||||
// The right-side lines of each changed C++ file's diff: the lines a
|
||||
// review comment can be placed on. A file too large for GitHub to
|
||||
// show its diff has none, and gets no suggestions.
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr.number, per_page: 100 });
|
||||
const ranges = {};
|
||||
for (const file of files) {
|
||||
if (file.status === 'removed' || !CPP.test(file.filename)) {
|
||||
continue;
|
||||
}
|
||||
ranges[file.filename] = [...(file.patch || '').matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)]
|
||||
.map((m) => [Number(m[1]), Number(m[1]) + Number(m[2] ?? 1) - 1])
|
||||
.filter(([first, last]) => last >= first);
|
||||
}
|
||||
|
||||
core.setOutput('pr', pr.number);
|
||||
core.setOutput('head-sha', pr.head.sha);
|
||||
core.setOutput('head-repo', run.head_repository.full_name);
|
||||
core.setOutput('ranges', JSON.stringify(ranges));
|
||||
core.setOutput('mention', mention);
|
||||
core.setOutput('request-id', request ? request.id : '');
|
||||
|
||||
- name: Download the fix
|
||||
if: steps.find.outputs.pr != ''
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: clang-tidy-fix
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: ${{ runner.temp }}/clang-tidy-fix
|
||||
|
||||
# The PR's own files, only to apply the patch to. Nothing in them is run.
|
||||
- name: Checkout the PR head
|
||||
if: steps.find.outputs.pr != ''
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.find.outputs.head-repo }}
|
||||
ref: ${{ steps.find.outputs.head-sha }}
|
||||
path: head
|
||||
fetch-depth: 1
|
||||
lfs: 'false'
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
src
|
||||
tests
|
||||
|
||||
# The fix was made on the PR's merge commit, so it is applied to the head
|
||||
# and diffed again, giving line numbers in the PR's own files.
|
||||
- name: Check the fix and place its suggestions
|
||||
id: place
|
||||
if: steps.find.outputs.pr != ''
|
||||
env:
|
||||
RANGES: ${{ steps.find.outputs.ranges }}
|
||||
run: |
|
||||
python3 -I - "$RUNNER_TEMP/clang-tidy-fix/fix.patch" head "$RUNNER_TEMP" <<'EOF'
|
||||
import json, os, re, subprocess, sys
|
||||
|
||||
patch_path, head, out = sys.argv[1:]
|
||||
ranges = json.loads(os.environ["RANGES"])
|
||||
|
||||
def output(**values):
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
||||
for key, value in values.items():
|
||||
f.write(f"{key}={value}\n")
|
||||
|
||||
patch = open(patch_path, encoding="utf-8").read()
|
||||
if not patch.strip():
|
||||
output(result="empty")
|
||||
sys.exit()
|
||||
|
||||
# Every line the patch adds must be an #include or blank, in a C++ file the
|
||||
# PR changes, and it may not remove, create, rename or change the mode of
|
||||
# anything.
|
||||
include = re.compile(r'\s*#\s*include\s*(<[\w./+-]+>|"[\w./+-]+")\s*')
|
||||
header = re.compile(r"(index [0-9a-f]+\.\.[0-9a-f]+( \d+)?|@@ -\d+(,\d+)? \+\d+(,\d+)? @@.*)")
|
||||
problems = []
|
||||
path = None
|
||||
for line in patch.splitlines():
|
||||
if line.startswith("diff --git "):
|
||||
match = re.fullmatch(r"diff --git a/(.+) b/\1", line)
|
||||
path = match.group(1) if match else None
|
||||
if path not in ranges:
|
||||
problems.append(f"{line}: not a C++ file the PR changes")
|
||||
# git appends a tab to the --- and +++ lines of a path that contains a space.
|
||||
elif line.removesuffix("\t") in (f"--- a/{path}", f"+++ b/{path}") or header.fullmatch(line):
|
||||
pass
|
||||
elif line.startswith(" ") or line == "\\ No newline at end of file":
|
||||
pass
|
||||
elif line.startswith("+") and (include.fullmatch(line[1:]) or not line[1:].strip()):
|
||||
pass
|
||||
else:
|
||||
problems.append(f"{path}: {line}")
|
||||
if problems:
|
||||
for problem in problems:
|
||||
print(f"::error::Refused patch line {problem}")
|
||||
output(result="rejected")
|
||||
sys.exit()
|
||||
|
||||
if subprocess.run(["git", "-C", head, "apply", "--index", os.path.abspath(patch_path)]).returncode != 0:
|
||||
output(result="apply-failed")
|
||||
sys.exit()
|
||||
|
||||
git = ["git", "-C", head, "-c", "core.quotePath=false", "diff", "--cached", "--no-color", "--no-ext-diff",
|
||||
"--src-prefix=a/", "--dst-prefix=b/"]
|
||||
with open(os.path.join(out, "head.patch"), "w", encoding="utf-8") as f:
|
||||
f.write(subprocess.run(git, check=True, capture_output=True, encoding="utf-8").stdout)
|
||||
|
||||
# Each run of added lines becomes one suggestion on the line it goes after
|
||||
# (before, at the top of a file): that line, replaced by itself and the
|
||||
# includes. It can only sit on a line the PR's diff shows.
|
||||
hunks = []
|
||||
path = None
|
||||
for line in subprocess.run(git + ["-U0"], check=True, capture_output=True, encoding="utf-8").stdout.splitlines():
|
||||
if line.startswith("diff --git "):
|
||||
path = re.fullmatch(r"diff --git a/(.+) b/\1", line).group(1)
|
||||
elif line.startswith("@@ "):
|
||||
hunks.append((path, int(re.match(r"@@ -(\d+)", line).group(1)), []))
|
||||
elif hunks and line.startswith("+") and not line.startswith("+++ "):
|
||||
hunks[-1][2].append(line[1:])
|
||||
|
||||
suggestions = []
|
||||
total = suggested = 0
|
||||
for path, after, added in hunks:
|
||||
count = sum(bool(include.fullmatch(line)) for line in added)
|
||||
total += count
|
||||
original = subprocess.run(["git", "-C", head, "show", f"HEAD:{path}"], check=True, capture_output=True,
|
||||
encoding="utf-8").stdout.splitlines()
|
||||
line = max(after, 1)
|
||||
if line > len(original) or not any(first <= line <= last for first, last in ranges[path]):
|
||||
continue
|
||||
body = "\n".join([original[line - 1], *added] if after else [*added, original[0]])
|
||||
# A fence longer than any run of backticks in the line it repeats.
|
||||
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", body)])
|
||||
suggestions.append({"path": path, "line": line, "side": "RIGHT",
|
||||
"body": f"{fence}suggestion\n{body}\n{fence}"})
|
||||
suggested += count
|
||||
with open(os.path.join(out, "suggestions.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(suggestions, f)
|
||||
output(result="ok", includes=total, suggested=suggested)
|
||||
EOF
|
||||
|
||||
- name: Post the fix
|
||||
if: always() && steps.find.outputs.pr != ''
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
PR: ${{ steps.find.outputs.pr }}
|
||||
MENTION: ${{ steps.find.outputs.mention }}
|
||||
REQUEST_ID: ${{ steps.find.outputs.request-id }}
|
||||
HEAD_SHA: ${{ steps.find.outputs.head-sha }}
|
||||
RESULT: ${{ steps.place.outputs.result }}
|
||||
INCLUDES: ${{ steps.place.outputs.includes }}
|
||||
SUGGESTED: ${{ steps.place.outputs.suggested }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const MARKER = '<!-- pr-tidy-bot -->';
|
||||
// GitHub's limit on a review body is 65536 characters.
|
||||
const MAX_PATCH = 60000;
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const { PR, MENTION, REQUEST_ID, HEAD_SHA, RESULT, INCLUDES, SUGGESTED, RUNNER_TEMP } = process.env;
|
||||
const issue_number = Number(PR);
|
||||
const sha = HEAD_SHA.slice(0, 10);
|
||||
const fixUrl = context.payload.workflow_run.html_url;
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||
|
||||
const react = (content) => REQUEST_ID
|
||||
? github.rest.reactions.createForIssueComment({ owner, repo, comment_id: Number(REQUEST_ID), content })
|
||||
: Promise.resolve();
|
||||
async function reply(body, content) {
|
||||
await react(content);
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body: `${MARKER}\n${MENTION}${body}` });
|
||||
}
|
||||
|
||||
const read = (...parts) => fs.readFileSync(path.join(RUNNER_TEMP, ...parts), 'utf8');
|
||||
const passes = RESULT && read('clang-tidy-fix', 'status').trim() === '0';
|
||||
|
||||
if (RESULT === 'empty') {
|
||||
return passes
|
||||
? reply(`clang-tidy passes on ${sha}. There is nothing to fix.`, '+1')
|
||||
: reply(`clang-tidy has no automatic fix for what it reports on ${sha}. The findings, in the [fix run's log](${fixUrl}), need a manual edit.`, '-1');
|
||||
}
|
||||
if (RESULT === 'rejected') {
|
||||
return reply(`the clang-tidy fix changes more than includes, so I didn't post it. See the [run log](${runUrl}).`, '-1');
|
||||
}
|
||||
if (RESULT === 'apply-failed') {
|
||||
return reply(`the clang-tidy fix doesn't apply to ${sha}. Merge the base branch into the PR, then comment \`/bot fix-tidy\` again.`, '-1');
|
||||
}
|
||||
if (RESULT !== 'ok') {
|
||||
return reply(`something went wrong. See the [run log](${runUrl}).`, '-1');
|
||||
}
|
||||
|
||||
const patch = read('head.patch');
|
||||
const suggestions = JSON.parse(read('suggestions.json'));
|
||||
|
||||
const one = INCLUDES === '1';
|
||||
const includes = one ? '1 missing include' : `${INCLUDES} missing includes`;
|
||||
const inline = SUGGESTED === '0'
|
||||
? (one ? 'It goes on a line this PR does not change, so I can\'t suggest it inline.'
|
||||
: 'They go on lines this PR does not change, so I can\'t suggest them inline.')
|
||||
: SUGGESTED === INCLUDES
|
||||
? (one ? 'It is suggested inline below.' : 'Each is suggested inline below.')
|
||||
: `${SUGGESTED} of them are suggested inline below; the rest go on lines this PR does not change.`;
|
||||
const remaining = passes
|
||||
? `With ${one ? 'it' : 'them'}, clang-tidy passes.`
|
||||
: `Some findings remain that need a manual edit; see the [fix run's log](${fixUrl}).`;
|
||||
// A fence longer than any run of backticks in the patch's context lines.
|
||||
const fence = '`'.repeat(Math.max(3, ...[...patch.matchAll(/`+/g)].map((m) => m[0].length + 1)));
|
||||
const apply = patch.length > MAX_PATCH
|
||||
? `The full patch is too large to show here. Download \`fix.patch\` from the [fix run](${fixUrl}) and run \`git apply fix.patch\`.`
|
||||
: `To apply ${one ? 'it' : 'all of them'}, run this in your checkout of the PR branch:\n\n${fence}sh\ngit apply <<'EOF'\n${patch}EOF\n${fence}`;
|
||||
const body = (withSuggestions) => `${MARKER}\n${MENTION}clang-tidy's fix for ${sha} adds ${includes}. ` +
|
||||
`${withSuggestions ? `${inline} ` : ''}${remaining}\n\n${apply}`;
|
||||
|
||||
const review = { owner, repo, pull_number: issue_number, commit_id: HEAD_SHA, event: 'COMMENT' };
|
||||
try {
|
||||
await github.rest.pulls.createReview({ ...review, body: body(true), comments: suggestions });
|
||||
} catch (error) {
|
||||
// A suggestion GitHub will not place still leaves the patch.
|
||||
core.warning(`Posting the suggestions failed (${error.message}); posting the patch alone.`);
|
||||
await github.rest.pulls.createReview({ ...review, body: body(false) });
|
||||
}
|
||||
await react('+1');
|
||||
|
||||
nudge:
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'workflow_run'
|
||||
&& github.event.workflow_run.name == 'clang-tidy'
|
||||
&& github.event.workflow_run.event == 'pull_request'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-nudge-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
actions: read # the check's steps
|
||||
pull-requests: write # comment on the PR
|
||||
issues: write # the comment
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Ask for a fix when the check fails
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const MARKER = '<!-- pr-tidy-bot-nudge -->';
|
||||
// The clang-tidy step's name in clang_tidy.yml. A failure in any other
|
||||
// step is the CI's, not the PR's.
|
||||
const TIDY_STEP = 'Run clang-tidy on the changed lines';
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const run = context.payload.workflow_run;
|
||||
|
||||
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||
});
|
||||
const pr = prs.find((p) => p.head.sha === run.head_sha);
|
||||
if (!pr) {
|
||||
core.info('No open PR at this run\'s commit; a newer run will answer.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ owner, repo, run_id: run.id });
|
||||
const failed = jobs.some((job) => (job.steps || []).some((step) => step.name === TIDY_STEP && step.conclusion === 'failure'));
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||
const previous = comments.find((c) => c.user.type === 'Bot' && (c.body || '').startsWith(MARKER));
|
||||
|
||||
if (!failed) {
|
||||
// Removes the request once the check passes.
|
||||
if (previous && run.conclusion === 'success') {
|
||||
await github.rest.issues.deleteComment({ owner, repo, comment_id: previous.id });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const body = `${MARKER}\n@${pr.user.login} the clang-tidy check found problems in this PR's changes ` +
|
||||
`at ${run.head_sha.slice(0, 10)} ([log](${run.html_url})). Missing includes can be fixed either way:\n\n` +
|
||||
'- run `scripts/run_clang_tidy.sh --fix` (`scripts\\run_clang_tidy.ps1 -Fix` on Windows) and push the result, or\n' +
|
||||
'- comment `/bot fix-tidy`, and I\'ll suggest the fix here for you to apply.\n\n' +
|
||||
'Other findings, such as a `using` in a header, need a manual edit.';
|
||||
// Edits the earlier request instead of posting another, so each push does not notify again.
|
||||
if (previous) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -497,47 +497,6 @@ endif()
|
||||
# Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
|
||||
# We pick it from environment if it is not defined in another way
|
||||
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
|
||||
# if(WIN32)
|
||||
# if(NOT DEFINED WIN10SDK_PATH)
|
||||
# if(DEFINED ENV{WIN10SDK_PATH})
|
||||
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
|
||||
# endif()
|
||||
# endif()
|
||||
# if(DEFINED WIN10SDK_PATH)
|
||||
# #BBS: modify win10sdk_path
|
||||
# if (EXISTS "${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h")
|
||||
# set(WIN10SDK_INCLUDE_PATH "${WIN10SDK_PATH}")
|
||||
# else()
|
||||
# message("WIN10SDK_PATH is invalid: ${WIN10SDK_PATH}")
|
||||
# message("${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h was not found")
|
||||
# message("STL fixing by the Netfabb service will not be compiled")
|
||||
# unset(WIN10SDK_PATH)
|
||||
# endif()
|
||||
# else()
|
||||
# # Try to use the default Windows 10 SDK path.
|
||||
# if (DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKVersion})
|
||||
# set(WIN10SDK_INCLUDE_PATH "$ENV{WindowsSdkDir}/Include/$ENV{WindowsSDKVersion}")
|
||||
# else ()
|
||||
# set(WIN10SDK_INCLUDE_PATH "C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0")
|
||||
# endif ()
|
||||
# if (NOT EXISTS "${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h")
|
||||
# message("${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h was not found")
|
||||
# message("STL fixing by the Netfabb service will not be compiled")
|
||||
# unset(WIN10SDK_INCLUDE_PATH)
|
||||
# endif()
|
||||
# endif()
|
||||
# if(WIN10SDK_INCLUDE_PATH)
|
||||
# message("Building with Win10 Netfabb STL fixing service support")
|
||||
# add_definitions(-DHAS_WIN10SDK)
|
||||
# include_directories(SYSTEM "${WIN10SDK_INCLUDE_PATH}")
|
||||
# else()
|
||||
# message("Building without Win10 Netfabb STL fixing service support")
|
||||
# endif()
|
||||
# endif()
|
||||
|
||||
if (APPLE)
|
||||
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")
|
||||
if (CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
|
||||
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
|
||||
# no headers for that object. ccache reproduces /showIncludes output on a hit.
|
||||
#
|
||||
# Clang also records the modification time of every input in a precompiled header
|
||||
# and rejects the header once one differs. ccache caches the compile that creates
|
||||
# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
|
||||
# with the old file's time, and every compile that uses it fails. The build system
|
||||
# already rebuilds the header when an input changes. This covers the dependencies,
|
||||
# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
|
||||
foreach (_lang C CXX)
|
||||
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
|
||||
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
|
||||
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
|
||||
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
|
||||
endif ()
|
||||
endforeach ()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Mesh repair — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
Mesh repair turns a broken triangle mesh into closed, outward oriented shells that bound a solid, so that the
|
||||
mesh slices predictably and the other mesh tools (booleans, cut, painting) get valid input. It runs on demand:
|
||||
the *Fix Model* command of the context menu of an object or part and of the warning icon in the object list, *Fix
|
||||
All* and *Fix All Plates* in the context menu of a plate, the prompt the Cut tool shows when a cut leaves open edges,
|
||||
the *Repair and import* choice of texture import, and the `--repair` option of the command line. It is built on CGAL
|
||||
and is the only repair the application has, on every platform.
|
||||
|
||||
`repair_mesh()` in `src/libslic3r/MeshRepair.cpp` does the work on one `TriangleMesh`, and `ModelVolume::repair()`
|
||||
applies it to a part: it replaces the mesh only when the repair changed it, and remaps or drops the painting of a
|
||||
replaced mesh. The GUI side, `fix_model_with_cgal_gui()` in `src/slic3r/Utils/FixModelByCgal.cpp`, runs it on a
|
||||
worker thread for each volume of an object, or for one volume, behind a progress dialog. `repair_mesh()` reports its
|
||||
progress between stages through a callback, which cancels the repair by throwing.
|
||||
|
||||
## What the repaired mesh is
|
||||
|
||||
The repair keeps what the slicer would print from the input. The slicer fills a layer where the winding number of
|
||||
the input surface is not zero (`MeshSlicingParams::SlicingMode::Regular` uses Clipper's nonzero fill rule), so:
|
||||
|
||||
- Shells crossing each other print as their union. The repair keeps them as separate shells, as the parts of an
|
||||
object are kept, and makes each of them an outward oriented solid.
|
||||
- A shell inside a solid that is oriented like it adds nothing, and the repair removes it.
|
||||
- A shell inside a solid that is oriented inward is a cavity, and stays one.
|
||||
- A whole mesh turned inside out prints as a solid, and the repair turns it outward.
|
||||
|
||||
On top of that, it closes holes and cracks, gives inconsistently oriented triangles the orientation of their
|
||||
neighbours, drops degenerate, duplicate and stray flat geometry, and removes small self-intersections inside a shell,
|
||||
such as slivers and folds. Where none of this applies, it reports the mesh as unmodified and the caller keeps the
|
||||
original mesh, its painting and its object id.
|
||||
|
||||
A volume is repaired in place and is never split into several parts: splitting by connectivity would turn a cavity
|
||||
into a separate solid part and fill it. Each solid of the result is a shell of its own, so *Split to parts* still
|
||||
separates them. Open pieces of the input whose borders meet exactly, which are pieces of one surface rather than
|
||||
solids, come out as the one shell they form.
|
||||
|
||||
## Pipeline
|
||||
|
||||
The input is treated as a triangle soup. Every stage after the first only runs on what the previous ones could
|
||||
not settle, so a valid mesh passes through quickly.
|
||||
|
||||
### Valid parts first
|
||||
|
||||
Parts of the input that are connected by vertex indices and are already closed, consistently oriented 2-manifolds
|
||||
skip the soup stages and go straight to the shell stages. Merging their points with coincident points of other
|
||||
parts would only make non-manifold junctions out of solids that touch, for example parts of an assembly exported
|
||||
together or voxels written as separate cubes.
|
||||
|
||||
### Soup clean-up
|
||||
|
||||
The rest of the soup has its coincident points merged. Triangles with repeated corners are dropped. Coincident
|
||||
triangles of opposite orientation are a wall between two solids touching face to face, so they cancel in pairs;
|
||||
of the remaining copies of a triangle one is kept. A connected region that would vanish entirely through
|
||||
cancelling is a two-sided export instead (every face written in both orientations), and keeps one copy of each
|
||||
triangle.
|
||||
|
||||
Open edges then get their vertices welded within a tolerance of 1e-4 of the bounding box diagonal, which closes the
|
||||
cracks some exporters leave between triangles that should share corners. A vertex is never welded further than a
|
||||
quarter of its shortest edge, so welding cannot collapse fine detail. A soup that stays mostly open after welding has
|
||||
cracks wider than that tolerance, and is welded again with ten and then a hundred times the tolerance.
|
||||
|
||||
### Work units
|
||||
|
||||
The soup is cut into units repaired independently and in parallel: its connected parts, except that open parts whose
|
||||
bounding boxes touch share a unit, so that cracks and T-junctions between separately exported patches still close.
|
||||
|
||||
### Building the mesh
|
||||
|
||||
A unit whose edges are each shared by at most two triangles, running in opposite directions, and whose vertices each
|
||||
have a single fan of triangles, becomes a `Surface_mesh` directly. Any other unit is oriented first. Triangles are
|
||||
flipped so that neighbours across an edge shared by two of them agree, each part keeping the orientation the
|
||||
majority of its area had, which keeps inward oriented cavities inward. Edges where that is impossible or that more
|
||||
than two triangles share are cut, and each fan of triangles around a vertex gets its own copy of the vertex. If that
|
||||
still leaves an invalid mesh, CGAL's `orient_polygon_soup()` orients the unit instead, and the parts it flipped
|
||||
against their majority are reversed back. The linear pass exists because `orient_polygon_soup()` is several times
|
||||
slower on large soups that need only a few fixes.
|
||||
|
||||
### Closing
|
||||
|
||||
Compatible borders are stitched, parts that can be stitched once reversed are merged, and borders still open are
|
||||
snapped onto each other within the weld tolerance, which closes T-junctions. Exactly degenerate faces are removed.
|
||||
|
||||
Holes are filled last. Planar border loops that face each other in one plane, one inside the other, bound a single
|
||||
region, such as a missing annulus at the end of a tube or a cap with features standing on it. Such a group is
|
||||
triangulated at once with a 2D constrained Delaunay triangulation that fills only the faces at odd nesting depth,
|
||||
so the openings inside the region stay open. The patch is used only if it closes every loop with the orientation
|
||||
of the triangles around it. Every other loop is filled on its own with `triangulate_hole()`, which uses a 2D
|
||||
constrained Delaunay triangulation when the loop is planar. Otherwise it searches the best triangulation among all
|
||||
triangles over the loop for loops of up to 100 edges, and among the triangles of a 3D Delaunay triangulation for
|
||||
longer ones, as the full search is cubic in the length of the loop. Beyond 200 edges the full search is not even
|
||||
used as a fallback, and a fan around the loop's centroid closes a loop nothing else could.
|
||||
|
||||
The closed unit is split into its shells. The largest stays in place and the others are copied out, as converting
|
||||
the whole mesh again costs seconds on large models.
|
||||
|
||||
### Shells
|
||||
|
||||
The shells of all units, together with the valid parts set aside at the start, are handled independently and in
|
||||
parallel. Shells enclosing no volume, which hole filling makes out of stray flat patches, are dropped.
|
||||
|
||||
Self-intersections inside a shell are removed locally with `experimental::remove_self_intersections()`, which
|
||||
remeshes the region around them. It is used only for local defects: a shell whose intersections involve more than
|
||||
5 % of its faces is left as it is, and a result that changes the enclosed volume by more than 0.1 % is undone. On
|
||||
heavily overlapping ornamental models the remeshing otherwise cuts parts down to slivers, while on genuine defects
|
||||
it changes the volume by thousandths of a percent. Overlaps left in place slice correctly. Remeshing can cut
|
||||
small pieces off a shell, so a shell it changed is split again and its pieces enclosing no volume are dropped.
|
||||
|
||||
Shells crossing others are found through their bounding boxes and an AABB tree per shell, and are made outward
|
||||
oriented solids. For every other shell the winding number just outside it is summed from the orientation of the
|
||||
shells containing it, tested from a point on its largest face. The shell bounds the solid only if the winding
|
||||
number is zero on exactly one of its sides; it is then oriented so that the solid is inside, or dropped otherwise.
|
||||
|
||||
Crossing shells are not united. Slicing unites them anyway, uniting them exactly costs tens of seconds on models
|
||||
made of many overlapping pieces, and it would fuse parts that *Split to parts* should keep apart.
|
||||
|
||||
## Robustness
|
||||
|
||||
The repair avoids `experimental::autorefine_and_remove_self_intersections()` and corefinement, which can crash on
|
||||
messy input, and uses only steps whose failures it can detect. A step that fails keeps its input. CGAL precondition
|
||||
failures surface as `repair_mesh()` returning false with the message as the error. The output is written in float,
|
||||
as every mesh in the application is.
|
||||
|
||||
The result does not depend on thread scheduling. `libslic3r_cgal` is built with `CGAL_LINKED_WITH_TBB`. Units and
|
||||
shells are processed in parallel, but each on its own data, and their results are gathered in a fixed order;
|
||||
sorts compare complete keys, and the detected self-intersections are put in order before they are used. Snapping
|
||||
stays sequential, as its parallel version gives different results from run to run. Holes are filled with the full
|
||||
search wherever it is affordable, because the search among Delaunay triangles can choose between equally good fills
|
||||
differently from run to run, so only non-planar holes of more than 100 edges may still vary. On some inputs
|
||||
`experimental::remove_self_intersections()` itself places a few vertices differently from run to run.
|
||||
|
||||
## Verification
|
||||
|
||||
`tests/libslic3r/test_mesh_repair.cpp` covers each kind of defect above on small meshes with known volumes.
|
||||
+38
-22
@@ -82,9 +82,7 @@ there.
|
||||
strong point is placed, no later strong modifier and no weak modifier is
|
||||
processed for that perimeter.
|
||||
- **Weak:** every weak modifier applies. They are applied from the lowest
|
||||
priority to the highest, so the highest one overwrites overlapping zones. A
|
||||
Blocked modifier that fully contains a perimeter is the exception: it is
|
||||
skipped there (see [Full containment](#full-containment)).
|
||||
priority to the highest, so the highest one overwrites overlapping zones.
|
||||
|
||||
A strong modifier without a usable segment, even one whose fragments were all
|
||||
discarded, passes the turn to the next one.
|
||||
@@ -232,20 +230,23 @@ begin and end positions on the source contour.
|
||||
### Full containment
|
||||
|
||||
A modifier that covers the whole perimeter has no boundaries on it. The policy
|
||||
follows seam painting, where painting a whole perimeter green is a meaningful
|
||||
choice and forbidding the seam all round is not:
|
||||
follows seam painting, where a whole perimeter can be painted in either color:
|
||||
|
||||
- **Seam Enforced** types the whole perimeter, like a perimeter painted green all
|
||||
round, with subdivision applied as described under [Weak modifiers](#weak-modifiers).
|
||||
- **Seam Neutral** types the whole perimeter Neutral, like an unmarked perimeter,
|
||||
clearing painting and lower zones.
|
||||
- **Seam Blocked** is skipped for the perimeter, with the full-containment
|
||||
warning. The seam cannot avoid the whole perimeter, so the modifier does not
|
||||
override anything below it: lower zones and painting stay in effect.
|
||||
- **Seam Center, Left and Right** are skipped with the same warning: there is no
|
||||
intersection to place the point on.
|
||||
- **Seam Blocked** types the whole perimeter Blocked, like a perimeter painted
|
||||
red all round, overriding painting and lower zones. The seam cannot avoid such
|
||||
a perimeter: the seam placer compares Blocked candidates with each other like
|
||||
Neutral ones, so the seam is placed as on an unmarked perimeter. If the
|
||||
perimeter stays Blocked after all weak zones, it is reported in the user
|
||||
warning (see [Diagnostics and warnings](#diagnostics-and-warnings)), so the
|
||||
user can check the modifier.
|
||||
- **Seam Center, Left and Right** are skipped with the full-containment warning:
|
||||
there is no intersection to place the point on.
|
||||
|
||||
Enforced and Neutral take part in the usual priority order (see
|
||||
Enforced, Neutral and Blocked take part in the usual priority order (see
|
||||
[Weak modifiers](#weak-modifiers)).
|
||||
|
||||
The perimeter is fully contained when the united intervals cover every source
|
||||
@@ -311,9 +312,9 @@ into every perimeter that has a strong seam.
|
||||
`collect_weak_modifier_segments()` extracts the segments of every weak modifier
|
||||
before the polygon is modified, so all positions refer to the same contour. Each
|
||||
segment becomes a zone with a type and two boundaries, kept in application
|
||||
order, lowest priority first. Full containment of an Enforced or Neutral
|
||||
modifier becomes a whole-perimeter zone at its place in that order: it has no
|
||||
boundaries and takes part in no insertion or helper step below. The boundaries
|
||||
order, lowest priority first. Full containment of a weak modifier becomes a
|
||||
whole-perimeter zone at its place in that order: it has no boundaries and takes
|
||||
part in no insertion or helper step below. The boundaries
|
||||
carry their positions on the source contour; these remain as provenance after
|
||||
insertion and are not indices into the modified polygon.
|
||||
|
||||
@@ -384,9 +385,10 @@ G-code export issues it as one non-critical warning with the ID
|
||||
`SlicingPreciseSeamWarning`. It is a single line, "Precise Seam: <causes>. Seam
|
||||
placement may differ from expected.", because the export warnings dialog shows
|
||||
only the first line of each warning. Repeated warning events replace the
|
||||
notification instead of appending to it. Except for the "had no effect" cause,
|
||||
the causes name the modifier types involved, as the menu names them, in menu
|
||||
order and each type once, for example "(Seam Left, Seam Enforced)".
|
||||
notification instead of appending to it. Except for the "covers an entire
|
||||
perimeter" and "had no effect" causes, the causes name the modifier types
|
||||
involved, as the menu names them, in menu order and each type once, for example
|
||||
"(Seam Left, Seam Enforced)".
|
||||
The causes are:
|
||||
|
||||
- **failed to process some intersections (types):** at least one fragment was
|
||||
@@ -395,8 +397,18 @@ The causes are:
|
||||
a Seam Center, Left or Right modifier had more than one segment on a
|
||||
perimeter (see [Strong modifiers](#strong-modifiers)).
|
||||
- **a perimeter is fully inside a modifier, the modifier was not applied to it
|
||||
(types):** a Seam Center, Left, Right or Blocked modifier was skipped for a
|
||||
(types):** a Seam Center, Left or Right modifier was skipped for a
|
||||
perimeter (see [Full containment](#full-containment)).
|
||||
- **Seam Blocked modifier "<name>" of "<object>" covers an entire perimeter:**
|
||||
a Seam Blocked modifier covered a whole perimeter (see
|
||||
[Full containment](#full-containment)). Only perimeters whose candidates are
|
||||
all Blocked after all weak zones contribute to this warning, and only through
|
||||
the zone in effect there: `apply_weak_modifiers_to_perimeter()` checks the
|
||||
final types and flags the modifier of the highest whole-perimeter zone when it
|
||||
is Blocked. A lower whole zone is overwritten by it and is not reported. Only
|
||||
the first flagged modifier in print and volume order is named, followed by
|
||||
"(N in total)" when there are several. The type name reuses the menu's
|
||||
translation.
|
||||
- **modifier "<name>" of "<object>" had no effect on the seam (it might not reach
|
||||
the centerline of the printed perimeter):** a modifier was evaluated on at
|
||||
least one perimeter and never gave a segment, full containment or a discarded
|
||||
@@ -420,6 +432,8 @@ The log records the following diagnostic markers:
|
||||
- `[PreciseSeamNoEffect]` for every modifier of the "had no effect" cause, with
|
||||
the object and modifier names. Unlike the user warning, the log lists all of
|
||||
them.
|
||||
- `[PreciseSeamFullyBlocked]` for every modifier of the "covers an entire
|
||||
perimeter" cause, listed the same way.
|
||||
|
||||
Failures and recoveries are counted separately. The first 10 of each per
|
||||
`init()` call are logged in detail, in parallel processing order; if a limit is
|
||||
@@ -563,12 +577,14 @@ last modifier would.
|
||||
priorities, weak boundaries that coincide or share an edge, enforced
|
||||
subdivision, whole-perimeter weak zones with painting and priorities, weak
|
||||
zones over painting's oversampled candidates, the warning type masks, usage
|
||||
tracking for the "had no effect" warning, volume sorting of strong and weak
|
||||
groups, restoration of strong points after alignment, raft layer indexing and
|
||||
structured slices. End-to-end tests slice a real object with Precise Seam
|
||||
tracking for the "had no effect" and whole-perimeter Blocked warnings, volume
|
||||
sorting of strong and weak groups, restoration of strong points after
|
||||
alignment, raft layer indexing and structured slices. End-to-end tests slice
|
||||
a real object with Precise Seam
|
||||
volumes and check the outer wall starts in the exported G-code: every strong
|
||||
mode under several seam positions and with a raft, Enforced and Blocked zones,
|
||||
a modifier with a hole, and the user warning.
|
||||
a perimeter blocked all round under every seam position, a modifier with a
|
||||
hole, and the user warning.
|
||||
- [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover
|
||||
enforced-patch selection independent of the contour start, fully painted
|
||||
contours, duplicate removal, and `Print::apply()` synchronization through type
|
||||
|
||||
+2163
-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
+4204
-2267
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 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.5,2.5h11v11h-11z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M8,2.5v11M2.5,8h11" style="fill:none;stroke:#949494;stroke-dasharray:1.5,1.5"/><path d="M4.5,11.5l3.5-6l3.5,6z" style="fill:#009688"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Bambulab",
|
||||
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
|
||||
"version": "02.08.00.14",
|
||||
"version": "02.08.00.15",
|
||||
"force_update": "0",
|
||||
"description": "the initial version of BBL configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -72,17 +72,21 @@ varying float weight;
|
||||
varying float island_active;
|
||||
varying vec2 vertex_uv;
|
||||
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
|
||||
// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
|
||||
// the preview wraps the texture the same way round as the bake.
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right
|
||||
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the
|
||||
// cylinder is to the viewer's right rather than mirrored.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
||||
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
||||
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
||||
@@ -94,7 +98,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -102,10 +106,18 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
vec3 an = abs(n); // Triplanar: drop the dominant normal axis
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0)
|
||||
// sits at the top of the model instead of upside down, and u is flipped on the far side of each
|
||||
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
|
||||
// siblings, term for term.
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
||||
@@ -124,7 +136,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
||||
float r = length(xy);
|
||||
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
||||
b = up; // v is the distance along the axis
|
||||
b = -up; // v is the distance *against* the axis
|
||||
return;
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
@@ -133,20 +145,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
||||
float c = length(dir.xy); // cos(latitude)
|
||||
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
||||
b = cross(dir, t); // increasing latitude, unit length
|
||||
b = -cross(dir, t); // v grows away from the pole, unit length
|
||||
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
||||
return;
|
||||
}
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
|
||||
t = vec3(0.0, 1.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else { // planar = p.xy
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 1.0, 0.0);
|
||||
vec3 an = abs(n); // the same signed basis projection_raw() uses
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z)
|
||||
t = vec3(0.0, box_face_sign(n.x), 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z)
|
||||
t = vec3(-box_face_sign(n.y), 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else { // planar = (+-x, -y)
|
||||
t = vec3(box_face_sign(n.z), 0.0, 0.0);
|
||||
b = vec3(0.0, -1.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,14 +36,20 @@ varying vec4 world_pos;
|
||||
varying float distortion;
|
||||
varying vec2 vertex_uv;
|
||||
|
||||
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
|
||||
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
||||
vec2 projection_raw(vec3 p, vec3 n)
|
||||
{
|
||||
@@ -53,7 +59,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -61,10 +67,16 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
|
||||
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
|
||||
vec3 an = abs(n);
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
|
||||
@@ -138,17 +138,21 @@ in vec2 vertex_uv;
|
||||
|
||||
out vec4 out_color;
|
||||
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
|
||||
// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
|
||||
// the preview wraps the texture the same way round as the bake.
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right
|
||||
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the
|
||||
// cylinder is to the viewer's right rather than mirrored.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
||||
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
||||
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
||||
@@ -160,7 +164,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -168,10 +172,18 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
vec3 an = abs(n); // Triplanar: drop the dominant normal axis
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0)
|
||||
// sits at the top of the model instead of upside down, and u is flipped on the far side of each
|
||||
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
|
||||
// siblings, term for term.
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
||||
@@ -190,7 +202,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
||||
float r = length(xy);
|
||||
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
||||
b = up; // v is the distance along the axis
|
||||
b = -up; // v is the distance *against* the axis
|
||||
return;
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
@@ -199,20 +211,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
||||
float c = length(dir.xy); // cos(latitude)
|
||||
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
||||
b = cross(dir, t); // increasing latitude, unit length
|
||||
b = -cross(dir, t); // v grows away from the pole, unit length
|
||||
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
||||
return;
|
||||
}
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
|
||||
t = vec3(0.0, 1.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else { // planar = p.xy
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 1.0, 0.0);
|
||||
vec3 an = abs(n); // the same signed basis projection_raw() uses
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z)
|
||||
t = vec3(0.0, box_face_sign(n.x), 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z)
|
||||
t = vec3(-box_face_sign(n.y), 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else { // planar = (+-x, -y)
|
||||
t = vec3(box_face_sign(n.z), 0.0, 0.0);
|
||||
b = vec3(0.0, -1.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,14 +47,20 @@ in vec2 vertex_uv;
|
||||
|
||||
out vec4 out_color;
|
||||
|
||||
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
|
||||
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
||||
vec2 projection_raw(vec3 p, vec3 n)
|
||||
{
|
||||
@@ -64,7 +70,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -72,10 +78,16 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
|
||||
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
|
||||
vec3 an = abs(n);
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script src="../js/plugin-config-ui.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<!-- <script type="text/javascript" src="./23.js"></script> -->
|
||||
<script src="./index.js"></script>
|
||||
@@ -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];
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/xterm/xterm.js"></script>
|
||||
<script type="text/javascript" src="../../include/xterm/xterm-addon-fit.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
|
||||
/*------------------ Date Function ------------------------*/
|
||||
function GetFullToday( )
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var nday=d.getDate();
|
||||
var nmonth=d.getMonth()+1;
|
||||
var nyear=d.getFullYear();
|
||||
|
||||
var strM=nmonth+'';
|
||||
if( nmonth<10 )
|
||||
strM='0'+nmonth;
|
||||
|
||||
var strD=nday+'';
|
||||
if( nday<10 )
|
||||
strD='0'+nday;
|
||||
|
||||
return nyear+'-'+strM+'-'+strD;
|
||||
}
|
||||
|
||||
function GetFullDate()
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
var strH=tDate.nhour+'';
|
||||
if( tDate.nhour<10 )
|
||||
strH='0'+tDate.nhour;
|
||||
|
||||
var strMin=tDate.nminute+'';
|
||||
if( tDate.nminute<10 )
|
||||
strMin='0'+tDate.nminute;
|
||||
|
||||
var strS=tDate.nsecond+'';
|
||||
if( tDate.nsecond<10 )
|
||||
strS='0'+tDate.nsecond;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
tDate.strFulldate=tDate.strdate+' '+strH+':'+strMin+':'+strS;
|
||||
|
||||
return tDate;
|
||||
}
|
||||
|
||||
|
||||
function Unixtimestamp2Date( nSecond )
|
||||
{
|
||||
var d=new Date(nSecond*1000);
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
|
||||
return tDate.strdate;
|
||||
}
|
||||
|
||||
|
||||
//------------Array Function-------------
|
||||
Array.prototype.in_array = function (e) {
|
||||
let sArray= ',' + this.join(this.S) + ',';
|
||||
let skey=','+e+',';
|
||||
|
||||
if(sArray.indexOf(skey)>=0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------String Function------------------
|
||||
/**
|
||||
* Delete Left/Right Side Blank
|
||||
*/
|
||||
String.prototype.trim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)|(\s*$)/g, '');
|
||||
}
|
||||
/**
|
||||
* Delete Left Side Blank
|
||||
*/
|
||||
String.prototype.ltrim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)/g,'');
|
||||
}
|
||||
/**
|
||||
* Delete Right Side Blank
|
||||
*/
|
||||
String.prototype.rtrim=function()
|
||||
{
|
||||
return this.replace(/(\s*$)/g,'');
|
||||
}
|
||||
|
||||
|
||||
//----------------Get Param-------------
|
||||
function GetQueryString(name)
|
||||
{
|
||||
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
|
||||
var r = window.location.search.substr(1).match(reg);
|
||||
if (r!=null)
|
||||
{
|
||||
return unescape(r[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function GetGetStr()
|
||||
{
|
||||
let strGet="";
|
||||
|
||||
//获取当前URL
|
||||
let url = document.location.href;
|
||||
|
||||
//获取?的位置
|
||||
let index = url.indexOf("?")
|
||||
if(index != -1) {
|
||||
//截取出?后面的字符串
|
||||
strGet = url.substr(index + 1);
|
||||
}
|
||||
|
||||
return strGet;
|
||||
}
|
||||
|
||||
|
||||
/*--------------------JSON Function------------*/
|
||||
|
||||
/*
|
||||
功能:检查一个字符串是不是标准的JSON格式
|
||||
参数: strJson 被检查的字符串
|
||||
返回值: 如果字符串是一个标准的JSON格式,则返回JSON对象
|
||||
如果字符串不是标准JSON格式,则返回null
|
||||
*/
|
||||
function IsJson( strJson )
|
||||
{
|
||||
var tJson=null;
|
||||
try
|
||||
{
|
||||
tJson=JSON.parse(strJson);
|
||||
}
|
||||
catch(exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return tJson;
|
||||
}
|
||||
|
||||
/*-----------------------Ajax Function--------------------*/
|
||||
/*对JQuery的Ajax函数的封装,只支持异步
|
||||
参数说明:
|
||||
url 目标地址
|
||||
action post/get
|
||||
data 字符串格式的发送内容
|
||||
asyn true---异步模式;false-----同步模式;
|
||||
*/
|
||||
function HttpReq( url,action, data,callbackfunc)
|
||||
{
|
||||
var strAction=action.toLowerCase();
|
||||
|
||||
if( strAction=="post")
|
||||
{
|
||||
$.post(url,data,callbackfunc);
|
||||
}
|
||||
else if( strAction=="get")
|
||||
{
|
||||
$.get(url,callbackfunc);
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------Cookie Function-------------------*/
|
||||
function setCookie(name, value, time='',path='') {
|
||||
if(time && path){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path="+path;
|
||||
}else if(time){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
|
||||
}else if(path){
|
||||
document.cookie = name + "=" + escape(value) + ";path="+path;
|
||||
}else{
|
||||
document.cookie = name + "=" + escape(value);
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(c_name)
|
||||
{
|
||||
if(document.cookie.length > 0) {
|
||||
c_start = document.cookie.indexOf(c_name + "=");//获取字符串的起点
|
||||
if(c_start != -1) {
|
||||
c_start = c_start + c_name.length + 1;//获取值的起点
|
||||
c_end = document.cookie.indexOf(";", c_start);//获取结尾处
|
||||
if(c_end == -1) c_end = document.cookie.length;//如果是最后一个,结尾就是cookie字符串的结尾
|
||||
return decodeURI(document.cookie.substring(c_start, c_end));//截取字符串返回
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function checkCookie(c_name) {
|
||||
username = getCookie(c_name);
|
||||
console.log(username);
|
||||
if (username != null && username != "")
|
||||
{ return true; }
|
||||
else
|
||||
{ return false; }
|
||||
}
|
||||
|
||||
function clearCookie(name) {
|
||||
setCookie(name, "", -1);
|
||||
}
|
||||
|
||||
|
||||
/*--------Studio WX Message-------*/
|
||||
function IsInSlicer()
|
||||
{
|
||||
let bMatch=navigator.userAgent.match( RegExp('BBL-Slicer','i') );
|
||||
|
||||
return bMatch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function SendWXMessage( strMsg )
|
||||
{
|
||||
let bCheck=IsInSlicer();
|
||||
|
||||
if(bCheck!=null)
|
||||
{
|
||||
window.wx.postMessage(strMsg);
|
||||
}
|
||||
}
|
||||
|
||||
/*------CSS Link Control----*/
|
||||
function RemoveCssLink( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
|
||||
let nTotal=pNow.length;
|
||||
for( let n=0;n<nTotal;n++ )
|
||||
{
|
||||
pNow[n].remove();
|
||||
}
|
||||
}
|
||||
|
||||
function AddCssLink( LinkPath )
|
||||
{
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var link = document.createElement('link');
|
||||
link.href = LinkPath;
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
||||
function CheckCssLinkExist( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
let nTotal=pNow.length;
|
||||
|
||||
return nTotal;
|
||||
}
|
||||
|
||||
|
||||
/*------Dark Mode------*/
|
||||
|
||||
function SwitchDarkMode( DarkCssPath )
|
||||
{
|
||||
ExecuteDarkMode( DarkCssPath );
|
||||
setInterval("ExecuteDarkMode('"+DarkCssPath+"')",1000);
|
||||
}
|
||||
|
||||
function ExecuteDarkMode( DarkCssPath )
|
||||
{
|
||||
let nMode=0;
|
||||
let bDarkMode=navigator.userAgent.match( RegExp('dark','i') );
|
||||
if( bDarkMode!=null )
|
||||
nMode=1;
|
||||
|
||||
let nNow=CheckCssLinkExist(DarkCssPath);
|
||||
if( nMode==0 )
|
||||
{
|
||||
if(nNow>0)
|
||||
RemoveCssLink(DarkCssPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(nNow==0)
|
||||
AddCssLink(DarkCssPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Dialog pages are themed by the injected --orca-* contract + theme.css; the legacy dark.css poll is fully retired.
|
||||
@@ -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");
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="load.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="1.css" />
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
</head>
|
||||
<body onLoad="TranslatePage()">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="11.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="../21/common.js"></script> <!-- ORCA use common sources for setup guide and standalone dialog -->
|
||||
<script type="text/javascript" src="21.js"></script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="../22/common.js"></script> <!-- ORCA use common sources for setup guide and standalone dialog -->
|
||||
<script type="text/javascript" src="./22.js"></script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="../22/common.js"></script> <!-- ORCA use common sources for setup guide and standalone dialog -->
|
||||
<script type="text/javascript" src="./23.js"></script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="../21/common.js"></script> <!-- ORCA use common sources for setup guide and standalone dialog -->
|
||||
<script type="text/javascript" src="24.js"></script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="4orca.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script type="text/javascript" src="../../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../js/json2.js"></script>
|
||||
<script type="text/javascript" src="../../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script type="text/javascript" src="5.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
|
||||
/*------------------ Date Function ------------------------*/
|
||||
function GetFullToday( )
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var nday=d.getDate();
|
||||
var nmonth=d.getMonth()+1;
|
||||
var nyear=d.getFullYear();
|
||||
|
||||
var strM=nmonth+'';
|
||||
if( nmonth<10 )
|
||||
strM='0'+nmonth;
|
||||
|
||||
var strD=nday+'';
|
||||
if( nday<10 )
|
||||
strD='0'+nday;
|
||||
|
||||
return nyear+'-'+strM+'-'+strD;
|
||||
}
|
||||
|
||||
function GetFullDate()
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
var strH=tDate.nhour+'';
|
||||
if( tDate.nhour<10 )
|
||||
strH='0'+tDate.nhour;
|
||||
|
||||
var strMin=tDate.nminute+'';
|
||||
if( tDate.nminute<10 )
|
||||
strMin='0'+tDate.nminute;
|
||||
|
||||
var strS=tDate.nsecond+'';
|
||||
if( tDate.nsecond<10 )
|
||||
strS='0'+tDate.nsecond;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
tDate.strFulldate=tDate.strdate+' '+strH+':'+strMin+':'+strS;
|
||||
|
||||
return tDate;
|
||||
}
|
||||
|
||||
|
||||
function Unixtimestamp2Date( nSecond )
|
||||
{
|
||||
var d=new Date(nSecond*1000);
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
|
||||
return tDate.strdate;
|
||||
}
|
||||
|
||||
|
||||
//------------Array Function-------------
|
||||
Array.prototype.in_array = function (e) {
|
||||
let sArray= ',' + this.join(this.S) + ',';
|
||||
let skey=','+e+',';
|
||||
|
||||
if(sArray.indexOf(skey)>=0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------String Function------------------
|
||||
/**
|
||||
* Delete Left/Right Side Blank
|
||||
*/
|
||||
String.prototype.trim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)|(\s*$)/g, '');
|
||||
}
|
||||
/**
|
||||
* Delete Left Side Blank
|
||||
*/
|
||||
String.prototype.ltrim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)/g,'');
|
||||
}
|
||||
/**
|
||||
* Delete Right Side Blank
|
||||
*/
|
||||
String.prototype.rtrim=function()
|
||||
{
|
||||
return this.replace(/(\s*$)/g,'');
|
||||
}
|
||||
|
||||
|
||||
//----------------Get Param-------------
|
||||
function GetQueryString(name)
|
||||
{
|
||||
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
|
||||
var r = window.location.search.substr(1).match(reg);
|
||||
if (r!=null)
|
||||
{
|
||||
return unescape(r[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function GetGetStr()
|
||||
{
|
||||
let strGet="";
|
||||
|
||||
//获取当前URL
|
||||
let url = document.location.href;
|
||||
|
||||
//获取?的位置
|
||||
let index = url.indexOf("?")
|
||||
if(index != -1) {
|
||||
//截取出?后面的字符串
|
||||
strGet = url.substr(index + 1);
|
||||
}
|
||||
|
||||
return strGet;
|
||||
}
|
||||
|
||||
|
||||
/*--------------------JSON Function------------*/
|
||||
|
||||
/*
|
||||
功能:检查一个字符串是不是标准的JSON格式
|
||||
参数: strJson 被检查的字符串
|
||||
返回值: 如果字符串是一个标准的JSON格式,则返回JSON对象
|
||||
如果字符串不是标准JSON格式,则返回null
|
||||
*/
|
||||
function IsJson( strJson )
|
||||
{
|
||||
var tJson=null;
|
||||
try
|
||||
{
|
||||
tJson=JSON.parse(strJson);
|
||||
}
|
||||
catch(exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return tJson;
|
||||
}
|
||||
|
||||
/*-----------------------Ajax Function--------------------*/
|
||||
/*对JQuery的Ajax函数的封装,只支持异步
|
||||
参数说明:
|
||||
url 目标地址
|
||||
action post/get
|
||||
data 字符串格式的发送内容
|
||||
asyn true---异步模式;false-----同步模式;
|
||||
*/
|
||||
function HttpReq( url,action, data,callbackfunc)
|
||||
{
|
||||
var strAction=action.toLowerCase();
|
||||
|
||||
if( strAction=="post")
|
||||
{
|
||||
$.post(url,data,callbackfunc);
|
||||
}
|
||||
else if( strAction=="get")
|
||||
{
|
||||
$.get(url,callbackfunc);
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------Cookie Function-------------------*/
|
||||
function setCookie(name, value, time='',path='') {
|
||||
if(time && path){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path="+path;
|
||||
}else if(time){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
|
||||
}else if(path){
|
||||
document.cookie = name + "=" + escape(value) + ";path="+path;
|
||||
}else{
|
||||
document.cookie = name + "=" + escape(value);
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(c_name)
|
||||
{
|
||||
if(document.cookie.length > 0) {
|
||||
c_start = document.cookie.indexOf(c_name + "=");//获取字符串的起点
|
||||
if(c_start != -1) {
|
||||
c_start = c_start + c_name.length + 1;//获取值的起点
|
||||
c_end = document.cookie.indexOf(";", c_start);//获取结尾处
|
||||
if(c_end == -1) c_end = document.cookie.length;//如果是最后一个,结尾就是cookie字符串的结尾
|
||||
return decodeURI(document.cookie.substring(c_start, c_end));//截取字符串返回
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function checkCookie(c_name) {
|
||||
username = getCookie(c_name);
|
||||
console.log(username);
|
||||
if (username != null && username != "")
|
||||
{ return true; }
|
||||
else
|
||||
{ return false; }
|
||||
}
|
||||
|
||||
function clearCookie(name) {
|
||||
setCookie(name, "", -1);
|
||||
}
|
||||
|
||||
|
||||
/*--------Studio WX Message-------*/
|
||||
function IsInSlicer()
|
||||
{
|
||||
let bMatch=navigator.userAgent.match( RegExp('BBL-Slicer','i') );
|
||||
|
||||
return bMatch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function SendWXMessage( strMsg )
|
||||
{
|
||||
let bCheck=IsInSlicer();
|
||||
|
||||
if(bCheck!=null)
|
||||
{
|
||||
window.wx.postMessage(strMsg);
|
||||
}
|
||||
}
|
||||
|
||||
/*------CSS Link Control----*/
|
||||
function RemoveCssLink( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
|
||||
let nTotal=pNow.length;
|
||||
for( let n=0;n<nTotal;n++ )
|
||||
{
|
||||
pNow[n].remove();
|
||||
}
|
||||
}
|
||||
|
||||
function AddCssLink( LinkPath )
|
||||
{
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var link = document.createElement('link');
|
||||
link.href = LinkPath;
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
||||
function CheckCssLinkExist( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
let nTotal=pNow.length;
|
||||
|
||||
return nTotal;
|
||||
}
|
||||
|
||||
|
||||
/*------Dark Mode------*/
|
||||
|
||||
function SwitchDarkMode( DarkCssPath )
|
||||
{
|
||||
ExecuteDarkMode( DarkCssPath );
|
||||
setInterval("ExecuteDarkMode('"+DarkCssPath+"')",1000);
|
||||
}
|
||||
|
||||
function ExecuteDarkMode( DarkCssPath )
|
||||
{
|
||||
let nMode=0;
|
||||
let bDarkMode=navigator.userAgent.match( RegExp('dark','i') );
|
||||
if( bDarkMode!=null )
|
||||
nMode=1;
|
||||
|
||||
let nNow=CheckCssLinkExist(DarkCssPath);
|
||||
if( nMode==0 )
|
||||
{
|
||||
if(nNow>0)
|
||||
RemoveCssLink(DarkCssPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(nNow==0)
|
||||
AddCssLink(DarkCssPath);
|
||||
}
|
||||
}
|
||||
|
||||
SwitchDarkMode( "../css/dark.css" );
|
||||
@@ -1,340 +0,0 @@
|
||||
|
||||
/*------------------ Date Function ------------------------*/
|
||||
function GetFullToday( )
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var nday=d.getDate();
|
||||
var nmonth=d.getMonth()+1;
|
||||
var nyear=d.getFullYear();
|
||||
|
||||
var strM=nmonth+'';
|
||||
if( nmonth<10 )
|
||||
strM='0'+nmonth;
|
||||
|
||||
var strD=nday+'';
|
||||
if( nday<10 )
|
||||
strD='0'+nday;
|
||||
|
||||
return nyear+'-'+strM+'-'+strD;
|
||||
}
|
||||
|
||||
function GetFullDate()
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
var strH=tDate.nhour+'';
|
||||
if( tDate.nhour<10 )
|
||||
strH='0'+tDate.nhour;
|
||||
|
||||
var strMin=tDate.nminute+'';
|
||||
if( tDate.nminute<10 )
|
||||
strMin='0'+tDate.nminute;
|
||||
|
||||
var strS=tDate.nsecond+'';
|
||||
if( tDate.nsecond<10 )
|
||||
strS='0'+tDate.nsecond;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
tDate.strFulldate=tDate.strdate+' '+strH+':'+strMin+':'+strS;
|
||||
|
||||
return tDate;
|
||||
}
|
||||
|
||||
|
||||
function Unixtimestamp2Date( nSecond )
|
||||
{
|
||||
var d=new Date(nSecond*1000);
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
|
||||
return tDate.strdate;
|
||||
}
|
||||
|
||||
|
||||
//------------Array Function-------------
|
||||
Array.prototype.in_array = function (e) {
|
||||
let sArray= ',' + this.join(this.S) + ',';
|
||||
let skey=','+e+',';
|
||||
|
||||
if(sArray.indexOf(skey)>=0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------String Function------------------
|
||||
/**
|
||||
* Delete Left/Right Side Blank
|
||||
*/
|
||||
String.prototype.trim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)|(\s*$)/g, '');
|
||||
}
|
||||
/**
|
||||
* Delete Left Side Blank
|
||||
*/
|
||||
String.prototype.ltrim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)/g,'');
|
||||
}
|
||||
/**
|
||||
* Delete Right Side Blank
|
||||
*/
|
||||
String.prototype.rtrim=function()
|
||||
{
|
||||
return this.replace(/(\s*$)/g,'');
|
||||
}
|
||||
|
||||
|
||||
//----------------Get Param-------------
|
||||
function GetQueryString(name)
|
||||
{
|
||||
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
|
||||
var r = window.location.search.substr(1).match(reg);
|
||||
if (r!=null)
|
||||
{
|
||||
return unescape(r[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function GetGetStr()
|
||||
{
|
||||
let strGet="";
|
||||
|
||||
//获取当前URL
|
||||
let url = document.location.href;
|
||||
|
||||
//获取?的位置
|
||||
let index = url.indexOf("?")
|
||||
if(index != -1) {
|
||||
//截取出?后面的字符串
|
||||
strGet = url.substr(index + 1);
|
||||
}
|
||||
|
||||
return strGet;
|
||||
}
|
||||
|
||||
|
||||
/*--------------------JSON Function------------*/
|
||||
|
||||
/*
|
||||
功能:检查一个字符串是不是标准的JSON格式
|
||||
参数: strJson 被检查的字符串
|
||||
返回值: 如果字符串是一个标准的JSON格式,则返回JSON对象
|
||||
如果字符串不是标准JSON格式,则返回null
|
||||
*/
|
||||
function IsJson( strJson )
|
||||
{
|
||||
var tJson=null;
|
||||
try
|
||||
{
|
||||
tJson=JSON.parse(strJson);
|
||||
}
|
||||
catch(exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return tJson;
|
||||
}
|
||||
|
||||
/*-----------------------Ajax Function--------------------*/
|
||||
/*对JQuery的Ajax函数的封装,只支持异步
|
||||
参数说明:
|
||||
url 目标地址
|
||||
action post/get
|
||||
data 字符串格式的发送内容
|
||||
asyn true---异步模式;false-----同步模式;
|
||||
*/
|
||||
function HttpReq( url,action, data,callbackfunc)
|
||||
{
|
||||
var strAction=action.toLowerCase();
|
||||
|
||||
if( strAction=="post")
|
||||
{
|
||||
$.post(url,data,callbackfunc);
|
||||
}
|
||||
else if( strAction=="get")
|
||||
{
|
||||
$.get(url,callbackfunc);
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------Cookie Function-------------------*/
|
||||
function setCookie(name, value, time='',path='') {
|
||||
if(time && path){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path="+path;
|
||||
}else if(time){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
|
||||
}else if(path){
|
||||
document.cookie = name + "=" + escape(value) + ";path="+path;
|
||||
}else{
|
||||
document.cookie = name + "=" + escape(value);
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(c_name)
|
||||
{
|
||||
if(document.cookie.length > 0) {
|
||||
c_start = document.cookie.indexOf(c_name + "=");//获取字符串的起点
|
||||
if(c_start != -1) {
|
||||
c_start = c_start + c_name.length + 1;//获取值的起点
|
||||
c_end = document.cookie.indexOf(";", c_start);//获取结尾处
|
||||
if(c_end == -1) c_end = document.cookie.length;//如果是最后一个,结尾就是cookie字符串的结尾
|
||||
return decodeURI(document.cookie.substring(c_start, c_end));//截取字符串返回
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function checkCookie(c_name) {
|
||||
username = getCookie(c_name);
|
||||
console.log(username);
|
||||
if (username != null && username != "")
|
||||
{ return true; }
|
||||
else
|
||||
{ return false; }
|
||||
}
|
||||
|
||||
function clearCookie(name) {
|
||||
setCookie(name, "", -1);
|
||||
}
|
||||
|
||||
|
||||
/*--------Studio WX Message-------*/
|
||||
function IsInSlicer()
|
||||
{
|
||||
let bMatch=navigator.userAgent.match( RegExp('BBL-Slicer','i') );
|
||||
|
||||
return bMatch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function SendWXMessage( strMsg )
|
||||
{
|
||||
let bCheck=IsInSlicer();
|
||||
|
||||
if(bCheck!=null)
|
||||
{
|
||||
window.wx.postMessage(strMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*------CSS Link Control----*/
|
||||
function RemoveCssLink( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
|
||||
let nTotal=pNow.length;
|
||||
for( let n=0;n<nTotal;n++ )
|
||||
{
|
||||
pNow[n].remove();
|
||||
}
|
||||
}
|
||||
|
||||
function AddCssLink( LinkPath )
|
||||
{
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var link = document.createElement('link');
|
||||
link.href = LinkPath;
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
||||
function CheckCssLinkExist( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
let nTotal=pNow.length;
|
||||
|
||||
return nTotal;
|
||||
}
|
||||
|
||||
|
||||
/*------Dark Mode------*/
|
||||
|
||||
function SwitchDarkMode( DarkCssPath )
|
||||
{
|
||||
ExecuteDarkMode( DarkCssPath );
|
||||
setInterval("ExecuteDarkMode('"+DarkCssPath+"')",1000);
|
||||
}
|
||||
|
||||
function ExecuteDarkMode( DarkCssPath )
|
||||
{
|
||||
let nMode=0;
|
||||
let bDarkMode=navigator.userAgent.match( RegExp('dark','i') );
|
||||
if( bDarkMode!=null )
|
||||
nMode=1;
|
||||
|
||||
let nNow=CheckCssLinkExist(DarkCssPath);
|
||||
if( nMode==0 )
|
||||
{
|
||||
if(nNow>0)
|
||||
RemoveCssLink(DarkCssPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(nNow==0)
|
||||
AddCssLink(DarkCssPath);
|
||||
}
|
||||
}
|
||||
|
||||
SwitchDarkMode("css/dark.css");
|
||||
@@ -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+'" >'+
|
||||
|
||||
+373
-427
@@ -1,427 +1,373 @@
|
||||
|
||||
/*------------------ Date Function ------------------------*/
|
||||
function GetFullToday( )
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var nday=d.getDate();
|
||||
var nmonth=d.getMonth()+1;
|
||||
var nyear=d.getFullYear();
|
||||
|
||||
var strM=nmonth+'';
|
||||
if( nmonth<10 )
|
||||
strM='0'+nmonth;
|
||||
|
||||
var strD=nday+'';
|
||||
if( nday<10 )
|
||||
strD='0'+nday;
|
||||
|
||||
return nyear+'-'+strM+'-'+strD;
|
||||
}
|
||||
|
||||
function GetFullDate()
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
var strH=tDate.nhour+'';
|
||||
if( tDate.nhour<10 )
|
||||
strH='0'+tDate.nhour;
|
||||
|
||||
var strMin=tDate.nminute+'';
|
||||
if( tDate.nminute<10 )
|
||||
strMin='0'+tDate.nminute;
|
||||
|
||||
var strS=tDate.nsecond+'';
|
||||
if( tDate.nsecond<10 )
|
||||
strS='0'+tDate.nsecond;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
tDate.strFulldate=tDate.strdate+' '+strH+':'+strMin+':'+strS;
|
||||
|
||||
return tDate;
|
||||
}
|
||||
|
||||
|
||||
function Unixtimestamp2Date( nSecond )
|
||||
{
|
||||
var d=new Date(nSecond*1000);
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
|
||||
return tDate.strdate;
|
||||
}
|
||||
|
||||
|
||||
//------------Array Function-------------
|
||||
Array.prototype.in_array = function (e) {
|
||||
let sArray= ',' + this.join(this.S) + ',';
|
||||
let skey=','+e+',';
|
||||
|
||||
if(sArray.indexOf(skey)>=0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------String Function------------------
|
||||
/**
|
||||
* Delete Left/Right Side Blank
|
||||
*/
|
||||
String.prototype.trim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)|(\s*$)/g, '');
|
||||
}
|
||||
/**
|
||||
* Delete Left Side Blank
|
||||
*/
|
||||
String.prototype.ltrim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)/g,'');
|
||||
}
|
||||
/**
|
||||
* Delete Right Side Blank
|
||||
*/
|
||||
String.prototype.rtrim=function()
|
||||
{
|
||||
return this.replace(/(\s*$)/g,'');
|
||||
}
|
||||
|
||||
|
||||
//----------------Get Param-------------
|
||||
function GetQueryString(name)
|
||||
{
|
||||
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
|
||||
var r = window.location.search.substr(1).match(reg);
|
||||
if (r!=null)
|
||||
{
|
||||
return unescape(r[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function GetGetStr()
|
||||
{
|
||||
let strGet="";
|
||||
|
||||
//获取当前URL
|
||||
let url = document.location.href;
|
||||
|
||||
//获取?的位置
|
||||
let index = url.indexOf("?")
|
||||
if(index != -1) {
|
||||
//截取出?后面的字符串
|
||||
strGet = url.substr(index + 1);
|
||||
}
|
||||
|
||||
return strGet;
|
||||
}
|
||||
|
||||
/*--------------File Function--------------*/
|
||||
function getFileName(path)
|
||||
{
|
||||
var pos1 = path.lastIndexOf('/');
|
||||
var pos2 = path.lastIndexOf('\\');
|
||||
var pos = Math.max(pos1, pos2);
|
||||
if (pos < 0) {
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return path.substring(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileTail(path)
|
||||
{
|
||||
var pos = path.lastIndexOf('.');
|
||||
if (pos < 0) {
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return path.substring(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/*--------------String Function-----------*/
|
||||
function html_encode(str)
|
||||
{
|
||||
var s = "";
|
||||
if (str.length == 0) return "";
|
||||
s = str.replace(/&/g, "&");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/ /g, " ");
|
||||
s = s.replace(/\'/g, "'");
|
||||
s = s.replace(/\"/g, """);
|
||||
s = s.replace(/\n/g, "<br/>");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function html_decode(str)
|
||||
{
|
||||
var s = "";
|
||||
if (str.length == 0) return "";
|
||||
s = str.replace(/&/g, "&");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/ /g, " ");
|
||||
s = s.replace(/'/g, "\'");
|
||||
s = s.replace(/"/g, "\"");
|
||||
s = s.replace(/<br\/>/g, "\n");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/*--------------------JSON Function------------*/
|
||||
|
||||
/*
|
||||
功能:检查一个字符串是不是标准的JSON格式
|
||||
参数: strJson 被检查的字符串
|
||||
返回值: 如果字符串是一个标准的JSON格式,则返回JSON对象
|
||||
如果字符串不是标准JSON格式,则返回null
|
||||
*/
|
||||
function IsJson( strJson )
|
||||
{
|
||||
var tJson=null;
|
||||
try
|
||||
{
|
||||
tJson=JSON.parse(strJson);
|
||||
}
|
||||
catch(exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return tJson;
|
||||
}
|
||||
|
||||
function DecodeJsonObject( pJson )
|
||||
{
|
||||
let tmpJson=JSON.stringify(pJson);
|
||||
tmpJson=decodeURIComponent(tmpJson);
|
||||
|
||||
pJson=JSON.parse(tmpJson);
|
||||
|
||||
return pJson;
|
||||
}
|
||||
|
||||
/*-----------------------Ajax Function--------------------*/
|
||||
/*对JQuery的Ajax函数的封装,只支持异步
|
||||
参数说明:
|
||||
url 目标地址
|
||||
action post/get
|
||||
data 字符串格式的发送内容
|
||||
asyn true---异步模式;false-----同步模式;
|
||||
*/
|
||||
function HttpReq( url,action, data,callbackfunc)
|
||||
{
|
||||
var strAction=action.toLowerCase();
|
||||
|
||||
if( strAction=="post")
|
||||
{
|
||||
$.post(url,data,callbackfunc);
|
||||
}
|
||||
else if( strAction=="get")
|
||||
{
|
||||
$.get(url,callbackfunc);
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------Cookie Function-------------------*/
|
||||
function setCookie(name, value, time='',path='') {
|
||||
if(time && path){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path="+path;
|
||||
}else if(time){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
|
||||
}else if(path){
|
||||
document.cookie = name + "=" + escape(value) + ";path="+path;
|
||||
}else{
|
||||
document.cookie = name + "=" + escape(value);
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(c_name)
|
||||
{
|
||||
if(document.cookie.length > 0) {
|
||||
c_start = document.cookie.indexOf(c_name + "=");//获取字符串的起点
|
||||
if(c_start != -1) {
|
||||
c_start = c_start + c_name.length + 1;//获取值的起点
|
||||
c_end = document.cookie.indexOf(";", c_start);//获取结尾处
|
||||
if(c_end == -1) c_end = document.cookie.length;//如果是最后一个,结尾就是cookie字符串的结尾
|
||||
return decodeURI(document.cookie.substring(c_start, c_end));//截取字符串返回
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function checkCookie(c_name) {
|
||||
username = getCookie(c_name);
|
||||
console.log(username);
|
||||
if (username != null && username != "")
|
||||
{ return true; }
|
||||
else
|
||||
{ return false; }
|
||||
}
|
||||
|
||||
function clearCookie(name) {
|
||||
setCookie(name, "", -1);
|
||||
}
|
||||
|
||||
|
||||
/*--------Studio WX Message-------*/
|
||||
function IsInSlicer()
|
||||
{
|
||||
let bMatch=navigator.userAgent.match( RegExp('BBL-Slicer','i') );
|
||||
|
||||
return bMatch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function SendWXMessage( strMsg )
|
||||
{
|
||||
let bCheck=IsInSlicer();
|
||||
|
||||
if(bCheck!=null)
|
||||
{
|
||||
window.wx.postMessage(strMsg);
|
||||
}
|
||||
}
|
||||
|
||||
function SendWXDebugInfo( strMsg )
|
||||
{
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="debug_info";
|
||||
tSend['msg']=strMsg;
|
||||
|
||||
SendWXMessage( JSON.stringify(tSend) );
|
||||
}
|
||||
|
||||
function OpenUrlInLocalBrowser( strUrl )
|
||||
{
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="common_openurl";
|
||||
tSend['url']=strUrl;
|
||||
|
||||
SendWXMessage( JSON.stringify(tSend) );
|
||||
}
|
||||
|
||||
|
||||
/*------CSS Link Control----*/
|
||||
function RemoveCssLink( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
|
||||
let nTotal=pNow.length;
|
||||
for( let n=0;n<nTotal;n++ )
|
||||
{
|
||||
pNow[n].remove();
|
||||
}
|
||||
}
|
||||
|
||||
function AddCssLink( LinkPath )
|
||||
{
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var link = document.createElement('link');
|
||||
link.href = LinkPath;
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
||||
function CheckCssLinkExist( LinkPath )
|
||||
{
|
||||
let pNow=$("head link[href='"+LinkPath+"']");
|
||||
let nTotal=pNow.length;
|
||||
|
||||
return nTotal;
|
||||
}
|
||||
|
||||
|
||||
/*------Dark Mode------*/
|
||||
|
||||
function SwitchDarkMode( DarkCssPath )
|
||||
{
|
||||
ExecuteDarkMode( DarkCssPath );
|
||||
setInterval("ExecuteDarkMode('"+DarkCssPath+"')",1000);
|
||||
}
|
||||
|
||||
function ExecuteDarkMode( DarkCssPath )
|
||||
{
|
||||
let nMode=0;
|
||||
let bDarkMode=navigator.userAgent.match( RegExp('dark','i') );
|
||||
if( bDarkMode!=null )
|
||||
nMode=1;
|
||||
|
||||
let nNow=CheckCssLinkExist(DarkCssPath);
|
||||
if( nMode==0 )
|
||||
{
|
||||
if(nNow>0)
|
||||
RemoveCssLink(DarkCssPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(nNow==0)
|
||||
AddCssLink(DarkCssPath);
|
||||
}
|
||||
}
|
||||
|
||||
SwitchDarkMode( "./css/dark.css" );
|
||||
|
||||
/*------------------ Date Function ------------------------*/
|
||||
function GetFullToday( )
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var nday=d.getDate();
|
||||
var nmonth=d.getMonth()+1;
|
||||
var nyear=d.getFullYear();
|
||||
|
||||
var strM=nmonth+'';
|
||||
if( nmonth<10 )
|
||||
strM='0'+nmonth;
|
||||
|
||||
var strD=nday+'';
|
||||
if( nday<10 )
|
||||
strD='0'+nday;
|
||||
|
||||
return nyear+'-'+strM+'-'+strD;
|
||||
}
|
||||
|
||||
function GetFullDate()
|
||||
{
|
||||
var d=new Date();
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
var strH=tDate.nhour+'';
|
||||
if( tDate.nhour<10 )
|
||||
strH='0'+tDate.nhour;
|
||||
|
||||
var strMin=tDate.nminute+'';
|
||||
if( tDate.nminute<10 )
|
||||
strMin='0'+tDate.nminute;
|
||||
|
||||
var strS=tDate.nsecond+'';
|
||||
if( tDate.nsecond<10 )
|
||||
strS='0'+tDate.nsecond;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
tDate.strFulldate=tDate.strdate+' '+strH+':'+strMin+':'+strS;
|
||||
|
||||
return tDate;
|
||||
}
|
||||
|
||||
|
||||
function Unixtimestamp2Date( nSecond )
|
||||
{
|
||||
var d=new Date(nSecond*1000);
|
||||
|
||||
var tDate={};
|
||||
|
||||
tDate.nyear=d.getFullYear();
|
||||
tDate.nmonth=d.getMonth()+1;
|
||||
tDate.nday=d.getDate();
|
||||
|
||||
tDate.nhour=d.getHours();
|
||||
tDate.nminute=d.getMinutes();
|
||||
tDate.nsecond=d.getSeconds();
|
||||
|
||||
tDate.nweek=d.getDay();
|
||||
tDate.ndate=d.getDate();
|
||||
|
||||
var strM=tDate.nmonth+'';
|
||||
if( tDate.nmonth<10 )
|
||||
strM='0'+tDate.nmonth;
|
||||
|
||||
var strD=tDate.nday+'';
|
||||
if( tDate.nday<10 )
|
||||
strD='0'+tDate.nday;
|
||||
|
||||
tDate.strdate=tDate.nyear+'-'+strM+'-'+strD;
|
||||
|
||||
return tDate.strdate;
|
||||
}
|
||||
|
||||
|
||||
//------------Array Function-------------
|
||||
Array.prototype.in_array = function (e) {
|
||||
let sArray= ',' + this.join(this.S) + ',';
|
||||
let skey=','+e+',';
|
||||
|
||||
if(sArray.indexOf(skey)>=0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------String Function------------------
|
||||
/**
|
||||
* Delete Left/Right Side Blank
|
||||
*/
|
||||
String.prototype.trim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)|(\s*$)/g, '');
|
||||
}
|
||||
/**
|
||||
* Delete Left Side Blank
|
||||
*/
|
||||
String.prototype.ltrim=function()
|
||||
{
|
||||
return this.replace(/(^\s*)/g,'');
|
||||
}
|
||||
/**
|
||||
* Delete Right Side Blank
|
||||
*/
|
||||
String.prototype.rtrim=function()
|
||||
{
|
||||
return this.replace(/(\s*$)/g,'');
|
||||
}
|
||||
|
||||
|
||||
//----------------Get Param-------------
|
||||
function GetQueryString(name)
|
||||
{
|
||||
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
|
||||
var r = window.location.search.substr(1).match(reg);
|
||||
if (r!=null)
|
||||
{
|
||||
return unescape(r[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function GetGetStr()
|
||||
{
|
||||
let strGet="";
|
||||
|
||||
//获取当前URL
|
||||
let url = document.location.href;
|
||||
|
||||
//获取?的位置
|
||||
let index = url.indexOf("?")
|
||||
if(index != -1) {
|
||||
//截取出?后面的字符串
|
||||
strGet = url.substr(index + 1);
|
||||
}
|
||||
|
||||
return strGet;
|
||||
}
|
||||
|
||||
/*--------------File Function--------------*/
|
||||
function getFileName(path)
|
||||
{
|
||||
var pos1 = path.lastIndexOf('/');
|
||||
var pos2 = path.lastIndexOf('\\');
|
||||
var pos = Math.max(pos1, pos2);
|
||||
if (pos < 0) {
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return path.substring(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileTail(path)
|
||||
{
|
||||
var pos = path.lastIndexOf('.');
|
||||
if (pos < 0) {
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return path.substring(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/*--------------String Function-----------*/
|
||||
function html_encode(str)
|
||||
{
|
||||
var s = "";
|
||||
if (str.length == 0) return "";
|
||||
s = str.replace(/&/g, "&");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/ /g, " ");
|
||||
s = s.replace(/\'/g, "'");
|
||||
s = s.replace(/\"/g, """);
|
||||
s = s.replace(/\n/g, "<br/>");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function html_decode(str)
|
||||
{
|
||||
var s = "";
|
||||
if (str.length == 0) return "";
|
||||
s = str.replace(/&/g, "&");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/ /g, " ");
|
||||
s = s.replace(/'/g, "\'");
|
||||
s = s.replace(/"/g, "\"");
|
||||
s = s.replace(/<br\/>/g, "\n");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/*--------------------JSON Function------------*/
|
||||
|
||||
/*
|
||||
功能:检查一个字符串是不是标准的JSON格式
|
||||
参数: strJson 被检查的字符串
|
||||
返回值: 如果字符串是一个标准的JSON格式,则返回JSON对象
|
||||
如果字符串不是标准JSON格式,则返回null
|
||||
*/
|
||||
function IsJson( strJson )
|
||||
{
|
||||
var tJson=null;
|
||||
try
|
||||
{
|
||||
tJson=JSON.parse(strJson);
|
||||
}
|
||||
catch(exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return tJson;
|
||||
}
|
||||
|
||||
function DecodeJsonObject( pJson )
|
||||
{
|
||||
let tmpJson=JSON.stringify(pJson);
|
||||
tmpJson=decodeURIComponent(tmpJson);
|
||||
|
||||
pJson=JSON.parse(tmpJson);
|
||||
|
||||
return pJson;
|
||||
}
|
||||
|
||||
/*-----------------------Ajax Function--------------------*/
|
||||
/*对JQuery的Ajax函数的封装,只支持异步
|
||||
参数说明:
|
||||
url 目标地址
|
||||
action post/get
|
||||
data 字符串格式的发送内容
|
||||
asyn true---异步模式;false-----同步模式;
|
||||
*/
|
||||
function HttpReq( url,action, data,callbackfunc)
|
||||
{
|
||||
var strAction=action.toLowerCase();
|
||||
|
||||
if( strAction=="post")
|
||||
{
|
||||
$.post(url,data,callbackfunc);
|
||||
}
|
||||
else if( strAction=="get")
|
||||
{
|
||||
$.get(url,callbackfunc);
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------Cookie Function-------------------*/
|
||||
function setCookie(name, value, time='',path='') {
|
||||
if(time && path){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path="+path;
|
||||
}else if(time){
|
||||
var strsec = time * 1000;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + strsec * 1);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
|
||||
}else if(path){
|
||||
document.cookie = name + "=" + escape(value) + ";path="+path;
|
||||
}else{
|
||||
document.cookie = name + "=" + escape(value);
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(c_name)
|
||||
{
|
||||
if(document.cookie.length > 0) {
|
||||
c_start = document.cookie.indexOf(c_name + "=");//获取字符串的起点
|
||||
if(c_start != -1) {
|
||||
c_start = c_start + c_name.length + 1;//获取值的起点
|
||||
c_end = document.cookie.indexOf(";", c_start);//获取结尾处
|
||||
if(c_end == -1) c_end = document.cookie.length;//如果是最后一个,结尾就是cookie字符串的结尾
|
||||
return decodeURI(document.cookie.substring(c_start, c_end));//截取字符串返回
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function checkCookie(c_name) {
|
||||
username = getCookie(c_name);
|
||||
console.log(username);
|
||||
if (username != null && username != "")
|
||||
{ return true; }
|
||||
else
|
||||
{ return false; }
|
||||
}
|
||||
|
||||
function clearCookie(name) {
|
||||
setCookie(name, "", -1);
|
||||
}
|
||||
|
||||
|
||||
/*--------Studio WX Message-------*/
|
||||
function IsInSlicer()
|
||||
{
|
||||
// window.wx is the host's message handler, registered on every webview OrcaSlicer creates.
|
||||
return !!(window.wx && window.wx.postMessage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function SendWXMessage( strMsg )
|
||||
{
|
||||
if( IsInSlicer() )
|
||||
window.wx.postMessage(strMsg);
|
||||
}
|
||||
|
||||
function SendWXDebugInfo( strMsg )
|
||||
{
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="debug_info";
|
||||
tSend['msg']=strMsg;
|
||||
|
||||
SendWXMessage( JSON.stringify(tSend) );
|
||||
}
|
||||
|
||||
function OpenUrlInLocalBrowser( strUrl )
|
||||
{
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="common_openurl";
|
||||
tSend['url']=strUrl;
|
||||
|
||||
SendWXMessage( JSON.stringify(tSend) );
|
||||
}
|
||||
|
||||
|
||||
/*------Dark Mode------*/
|
||||
// The host sets window.__orcaTheme at document start (WebView::AddHostThemeScript) and reloads the
|
||||
// page when the theme changes. Every legacy page links its dark.css up front; it is switched on and
|
||||
// off here instead of being added and removed, so the page's own path to it does not matter.
|
||||
function ExecuteDarkMode()
|
||||
{
|
||||
let bDarkMode = window.__orcaTheme === "dark";
|
||||
document.querySelectorAll('head link[href$="dark.css"]').forEach( function(link) { link.disabled = !bDarkMode; } );
|
||||
}
|
||||
|
||||
ExecuteDarkMode();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script type="text/javascript" src="../include/i18n.js"></script>
|
||||
<script type="text/javascript" src="../include/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../homepage/js/json2.js"></script>
|
||||
<script type="text/javascript" src="../homepage/js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../include/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../homepage/js/home.js"></script>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
|
||||
@@ -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" ;;
|
||||
|
||||
+43
-17
@@ -1,21 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Lets a pull request's macOS deps build wait for a push build of the base
|
||||
# branch that builds the same deps, from a Linux job. `check` runs after a cache
|
||||
# miss and names a queued or running base build with the same deps/ and .github/
|
||||
# to wait for, unless the pull request changes either. `wait` waits until that
|
||||
# build saves KEY. actions/cache then confirms the entry can be restored.
|
||||
# Lets a pull request's macOS deps build, or its clang-tidy job, wait for a
|
||||
# build that saves the deps cache it needs, from a Linux job. `check` runs after
|
||||
# a cache miss and names a queued or running push build of the base branch with
|
||||
# the same deps/ and .github/ to wait for, unless the pull request changes either.
|
||||
# `own` names the pull request's own build instead, for clang-tidy when the pull
|
||||
# request changes deps/. `wait` waits until that build saves KEY under CACHE_REF,
|
||||
# or its deps job fails. actions/cache then confirms the entry can be restored.
|
||||
#
|
||||
# Environment: GH_TOKEN, REPO, WORKFLOW_REF and BASE_REF, plus KEY_INPUTS,
|
||||
# WAIT_FOR, WAIT_SINCE, WAIT_MINUTES, KEY and ARCH for `wait`.
|
||||
# Environment: GH_TOKEN, REPO, WORKFLOW_REF (or WORKFLOW, the build workflow's
|
||||
# file name) and BASE_REF, plus HEAD_SHA for `own`, and KEY_INPUTS, WAIT_FOR,
|
||||
# WAIT_SINCE, WAIT_MINUTES, KEY, CACHE_REF (default the base branch) and ARCH
|
||||
# (or JOB, the matrix job's name) for `wait`.
|
||||
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
|
||||
set -uo pipefail
|
||||
|
||||
workflow=${WORKFLOW_REF%%@*}
|
||||
workflow=${workflow##*/}
|
||||
workflow=${WORKFLOW_REF:-}
|
||||
workflow=${workflow%%@*}
|
||||
workflow=${WORKFLOW:-${workflow##*/}}
|
||||
|
||||
# Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml.
|
||||
check_job_name="build_macos_arch (${ARCH:-}) / Check Cache"
|
||||
deps_job_name="build_macos_arch (${ARCH:-}) / Build Deps / Build Deps"
|
||||
job=${JOB:-build_macos_arch (${ARCH:-})}
|
||||
check_job_name="$job / Check Cache"
|
||||
deps_job_name="$job / Build Deps / Build Deps"
|
||||
|
||||
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
|
||||
|
||||
@@ -63,8 +69,27 @@ check() {
|
||||
fi
|
||||
}
|
||||
|
||||
own() {
|
||||
out "key-inputs=$(local_key_inputs HEAD)"
|
||||
local id
|
||||
# The build can be created a moment after this job.
|
||||
for _ in 1 2 3 4 5; do
|
||||
id=$(gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \
|
||||
-f head_sha="$HEAD_SHA" -f event=pull_request -f per_page=1 --jq '.workflow_runs[0].id // empty' < /dev/null)
|
||||
[ -n "$id" ] && break
|
||||
sleep 30
|
||||
done
|
||||
if [ -n "$id" ]; then
|
||||
echo "Waiting for the deps cache of build $id of this pull request."
|
||||
out "wait-for=$id"
|
||||
out "wait-since=$(date +%s)"
|
||||
else
|
||||
echo "No build of this pull request is building its deps."
|
||||
fi
|
||||
}
|
||||
|
||||
key_saved() {
|
||||
gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \
|
||||
gh cache list -R "$REPO" --ref "${CACHE_REF:-refs/heads/$BASE_REF}" --key "$KEY" --json key < /dev/null \
|
||||
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
|
||||
}
|
||||
|
||||
@@ -87,11 +112,11 @@ wait_for() {
|
||||
key_saved && { echo "Found $KEY."; return; }
|
||||
sleep 20
|
||||
done
|
||||
echo "Build $id of $BASE_REF finished its deps without saving $KEY."
|
||||
echo "Build $id finished its deps without saving $KEY."
|
||||
return ;;
|
||||
"completed cancelled") ;;
|
||||
completed*)
|
||||
echo "The deps job of build $id of $BASE_REF did not succeed."
|
||||
echo "The deps job of build $id did not succeed."
|
||||
return ;;
|
||||
# The deps job exists only once the build's cache check has succeeded.
|
||||
"")
|
||||
@@ -100,7 +125,7 @@ wait_for() {
|
||||
completed*)
|
||||
check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs")
|
||||
if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then
|
||||
echo "::warning title=Deps wait::Build $id of $BASE_REF has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
|
||||
echo "::warning title=Deps wait::Build $id has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
|
||||
warned=1
|
||||
fi ;;
|
||||
*)
|
||||
@@ -118,11 +143,12 @@ wait_for() {
|
||||
seen="$seen $id"
|
||||
echo "Following build $id of $BASE_REF."
|
||||
done
|
||||
echo "No build of $BASE_REF saved $KEY."
|
||||
echo "No build saved $KEY."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
check) check ;;
|
||||
own) own ;;
|
||||
wait) wait_for ;;
|
||||
*) echo "usage: $0 check|wait" >&2; exit 2 ;;
|
||||
*) echo "usage: $0 check|own|wait" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
@@ -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"})
|
||||
|
||||
+28
-6
@@ -99,6 +99,7 @@
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/GCode.hpp"
|
||||
#include "libslic3r/MeshRepair.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/ModelArrange.hpp"
|
||||
#include "libslic3r/Platform.hpp"
|
||||
@@ -206,7 +207,7 @@ std::map<int, std::string> cli_errors = {
|
||||
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
|
||||
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
|
||||
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "One of the plate is empty or has no object fully inside it. Please check that the 3mf contains no empty plate in Orca Slicer before uploading."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
|
||||
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
|
||||
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
|
||||
{CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."},
|
||||
@@ -1463,7 +1464,7 @@ int CLI::run(int argc, char **argv)
|
||||
//int arrange_option;
|
||||
int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1;
|
||||
bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false;
|
||||
bool allow_rotations = true, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
|
||||
bool allow_rotations = false, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
|
||||
Semver file_version;
|
||||
std::map<size_t, bool> orients_requirement;
|
||||
std::vector<Preset*> project_presets;
|
||||
@@ -5130,9 +5131,19 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "repair") {
|
||||
// Models are repaired by default.
|
||||
//for (auto &model : m_models)
|
||||
// model.repair();
|
||||
if (m_config.opt_bool(opt_key))
|
||||
for (Model &model : m_models)
|
||||
for (ModelObject *object : model.objects) {
|
||||
for (ModelVolume *volume : object->volumes) {
|
||||
MeshRepairStats stats;
|
||||
std::string error;
|
||||
if (!volume->repair(true, &stats, &error))
|
||||
BOOST_LOG_TRIVIAL(warning) << "repair of " << object->name << " / " << volume->name << " failed: " << error;
|
||||
else if (stats.modified)
|
||||
BOOST_LOG_TRIVIAL(info) << "repaired " << object->name << " / " << volume->name;
|
||||
}
|
||||
object->invalidate_bounding_box();
|
||||
}
|
||||
} else {
|
||||
boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info);
|
||||
@@ -5142,6 +5153,7 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n";
|
||||
bool oriented_or_arranged = false;
|
||||
bool arranged_without_rotations = false;
|
||||
//BBS: add orient and arrange logic here
|
||||
for (auto& model : m_models)
|
||||
{
|
||||
@@ -5409,6 +5421,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1);
|
||||
arrangement::arrange(selected, unselected, beds, arrange_cfg);
|
||||
//arrangement::arrange(unprintable, {}, beds, arrange_cfg);
|
||||
arranged_without_rotations |= !arrange_cfg.allow_rotations;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1);
|
||||
|
||||
//step-4: postprocess the bed index and result
|
||||
@@ -5881,6 +5894,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count;
|
||||
arrangement::arrange(selected, unselected, beds, arrange_cfg);
|
||||
arrangement::arrange(unprintable, {}, beds, arrange_cfg);
|
||||
arranged_without_rotations |= !arrange_cfg.allow_rotations;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count;
|
||||
|
||||
//Step-4:postprocess by partplate list&&apply the result
|
||||
@@ -6425,7 +6439,15 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
if (count == 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, cli_errors[CLI_NO_SUITABLE_OBJECTS], sliced_info);
|
||||
std::string no_objects_reason = cli_errors[CLI_NO_SUITABLE_OBJECTS];
|
||||
if (arranged_without_rotations) {
|
||||
//ORCA: result.json is only written on Linux, so also print the reason to stderr.
|
||||
no_objects_reason = "No object is fully inside the print volume. Arrange ran without rotations."
|
||||
" If an object only fits the bed when rotated, pass --allow-rotations."
|
||||
" Otherwise the plate is empty or an object is larger than the printable area or height.";
|
||||
boost::nowide::cerr << "plate " << index+1 << ": " << no_objects_reason << std::endl;
|
||||
}
|
||||
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, no_objects_reason, sliced_info);
|
||||
flush_and_exit(CLI_NO_SUITABLE_OBJECTS);
|
||||
}
|
||||
else if ((plate_to_slice != 0) || pre_check) {
|
||||
|
||||
@@ -1374,6 +1374,26 @@ bool AppConfig::write_config_file(const std::string &path, std::string body, con
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppConfig::clear_local_machine_access_code(const std::string& dev_id, const std::string& agent_id)
|
||||
{
|
||||
// Unscoped credentials predate printer agents and belong to the legacy BBL agent.
|
||||
const bool is_bbl_agent = agent_id.empty() || agent_id == "bbl";
|
||||
auto it = m_local_machines.find(dev_id);
|
||||
if (it != m_local_machines.end()) {
|
||||
const std::string& stored_agent = it->second.printer_agent_id;
|
||||
if (stored_agent == agent_id || (is_bbl_agent && (stored_agent.empty() || stored_agent == "bbl"))) {
|
||||
if (!it->second.access_code.empty()) {
|
||||
it->second.access_code.clear();
|
||||
m_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_bbl_agent) {
|
||||
erase("access_code", dev_id);
|
||||
erase("user_access_code", dev_id);
|
||||
}
|
||||
}
|
||||
|
||||
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
||||
{
|
||||
const auto it_v = m_vendors.find(vendor);
|
||||
|
||||
@@ -306,6 +306,8 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void clear_local_machine_access_code(const std::string& dev_id, const std::string& agent_id);
|
||||
|
||||
const std::vector<std::string> &get_filament_presets() const { return m_filament_presets; }
|
||||
void set_filament_presets(const std::vector<std::string> &filament_presets){
|
||||
m_filament_presets = filament_presets;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -625,6 +625,7 @@ add_library(libslic3r_cgal STATIC
|
||||
CutSurface.hpp CutSurface.cpp
|
||||
IntersectionPoints.hpp IntersectionPoints.cpp
|
||||
MeshBoolean.hpp MeshBoolean.cpp
|
||||
MeshRepair.hpp MeshRepair.cpp
|
||||
TryCatchSignal.hpp TryCatchSignal.cpp
|
||||
Triangulation.hpp Triangulation.cpp
|
||||
)
|
||||
@@ -652,7 +653,9 @@ if (IS_CLANG_CL)
|
||||
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs)
|
||||
# Lets CGAL algorithms given CGAL::Parallel_if_available_tag run on TBB.
|
||||
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_LINKED_WITH_TBB)
|
||||
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs TBB::tbb)
|
||||
|
||||
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
|
||||
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -236,19 +237,29 @@ static void getNamedSolids(const TopLoc_Location& location,
|
||||
int i = 0;
|
||||
switch (shape_type) {
|
||||
case TopAbs_COMPOUND:
|
||||
if (!isSplitCompound) {
|
||||
namedSolids.emplace_back(TopoDS::Compound(transform.Shape()), fullName);
|
||||
break;
|
||||
}
|
||||
case TopAbs_COMPSOLID:
|
||||
if (!isSplitCompound) {
|
||||
namedSolids.emplace_back(TopoDS::CompSolid(transform.Shape()), fullName);
|
||||
namedSolids.emplace_back(transform.Shape(), fullName);
|
||||
} else {
|
||||
for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) {
|
||||
i++;
|
||||
const TopoDS_Shape& currentShape = explorer.Current();
|
||||
namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i));
|
||||
}
|
||||
// Surface models have no solids: each shell outside a solid is a part, and loose faces stay together.
|
||||
i = 0;
|
||||
for (explorer.Init(transform.Shape(), TopAbs_SHELL, TopAbs_SOLID); explorer.More(); explorer.Next())
|
||||
namedSolids.emplace_back(explorer.Current(), fullName + "-SHELL-" + std::to_string(++i));
|
||||
TopoDS_Compound loose_faces;
|
||||
TopoDS_Builder builder;
|
||||
builder.MakeCompound(loose_faces);
|
||||
bool has_loose_faces = false;
|
||||
for (explorer.Init(transform.Shape(), TopAbs_FACE, TopAbs_SHELL); explorer.More(); explorer.Next()) {
|
||||
builder.Add(loose_faces, explorer.Current());
|
||||
has_loose_faces = true;
|
||||
}
|
||||
if (has_loose_faces)
|
||||
namedSolids.emplace_back(loose_faces, fullName);
|
||||
}
|
||||
break;
|
||||
case TopAbs_SOLID:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "libslic3r/BrimEarsPoint.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include "bbs_3mf.hpp"
|
||||
@@ -354,6 +355,13 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
|
||||
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
|
||||
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
|
||||
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
|
||||
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
|
||||
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
|
||||
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
|
||||
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
|
||||
// BBS
|
||||
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
||||
|
||||
@@ -847,6 +855,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::vector<std::string> custom_seam;
|
||||
std::vector<std::string> mmu_segmentation;
|
||||
std::vector<std::string> fuzzy_skin;
|
||||
// One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
|
||||
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
|
||||
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
|
||||
// BBS
|
||||
std::vector<std::string> face_properties;
|
||||
|
||||
@@ -867,6 +878,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
custom_seam.clear();
|
||||
mmu_segmentation.clear();
|
||||
fuzzy_skin.clear();
|
||||
for (std::vector<std::string> &slot : texture_displacement)
|
||||
slot.clear();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -974,6 +987,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
MetadataList metadata;
|
||||
VolumeMetadataList volumes;
|
||||
bool auto_drop { true };
|
||||
};
|
||||
|
||||
struct CutObjectInfo
|
||||
@@ -1200,6 +1214,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
/*IdToSlaSupportPointsMap m_sla_support_points;
|
||||
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
||||
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
|
||||
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
|
||||
// are built only after the whole archive has been walked, so by the time a volume names its JSON
|
||||
// every file it could refer to is already in here, whatever order the archive happened to be in.
|
||||
std::map<std::string, std::string> m_texture_displacement_files;
|
||||
void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
|
||||
std::string m_curr_metadata_name;
|
||||
std::string m_curr_characters;
|
||||
std::string m_name;
|
||||
@@ -1349,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);
|
||||
|
||||
@@ -2073,6 +2092,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
else if (_is_svg_shape_file(name)) {
|
||||
_extract_embossed_svg_shape_file(name, archive, stat);
|
||||
}
|
||||
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
|
||||
std::string contents(stat.m_uncomp_size, '\0');
|
||||
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
|
||||
m_texture_displacement_files.emplace(name, std::move(contents));
|
||||
else
|
||||
add_error("Error while reading texture displacement data");
|
||||
}
|
||||
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
|
||||
m_parsing_slice_info = true;
|
||||
//extract slice info from archive
|
||||
@@ -2234,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")
|
||||
@@ -3310,6 +3338,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
}*/
|
||||
|
||||
// Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
|
||||
// being built, which is after the whole archive has been walked - so every file it can refer to is
|
||||
// already extracted, and no deferral is needed.
|
||||
//
|
||||
// A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
|
||||
// loaded, which is the same state as a project saved by a build without the feature: recoverable by
|
||||
// picking the texture again rather than a hard failure.
|
||||
void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
|
||||
{
|
||||
const auto json = m_texture_displacement_files.find(json_path);
|
||||
if (json == m_texture_displacement_files.end()) {
|
||||
add_error("Missing texture displacement data: " + json_path);
|
||||
return;
|
||||
}
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementOptions options;
|
||||
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
|
||||
add_error("Malformed texture displacement data: " + json_path);
|
||||
return;
|
||||
}
|
||||
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
|
||||
// the project must never read the file it names: that would let whoever made the project pick any
|
||||
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
|
||||
// `path` stays a label of where the texture came from; a layer the archive carries no image for
|
||||
// loads without one, its settings intact, until the user picks a texture again.
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
if (layer.path_in_3mf.empty())
|
||||
continue;
|
||||
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
|
||||
if (image == m_texture_displacement_files.end()) {
|
||||
add_error("Missing texture displacement image: " + layer.path_in_3mf);
|
||||
continue;
|
||||
}
|
||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(), image->second.end());
|
||||
}
|
||||
volume.texture_displacement_layers = std::move(layers);
|
||||
volume.texture_displacement_options = options;
|
||||
}
|
||||
|
||||
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
|
||||
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
|
||||
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
||||
@@ -3966,6 +4033,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
||||
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
|
||||
mask.resize(m_curr_object->geometry.triangles.size());
|
||||
mask.back() = paint;
|
||||
}
|
||||
// BBS
|
||||
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -4047,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()
|
||||
@@ -4277,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;
|
||||
|
||||
@@ -4314,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);
|
||||
|
||||
@@ -4347,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);
|
||||
}
|
||||
@@ -4418,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;
|
||||
}
|
||||
@@ -5317,6 +5388,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->mmu_segmentation_facets.touch();
|
||||
volume->fuzzy_skin_facets.shrink_to_fit();
|
||||
volume->fuzzy_skin_facets.touch();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<std::string> &mask = sub_object->geometry.texture_displacement[slot];
|
||||
if (mask.empty())
|
||||
continue; // no triangle painted on this slot
|
||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
||||
facets.reserve(triangles_count);
|
||||
for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
|
||||
if (!mask[i].empty())
|
||||
facets.set_triangle_from_string(i, mask[i]);
|
||||
facets.shrink_to_fit();
|
||||
facets.touch();
|
||||
}
|
||||
}
|
||||
|
||||
volume->set_type(volume_data->part_type);
|
||||
@@ -5332,6 +5415,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
for (const Metadata& metadata : volume_data->metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5496,12 +5581,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->seam_facets.shrink_to_fit();
|
||||
volume->mmu_segmentation_facets.shrink_to_fit();
|
||||
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<std::string> &mask = geometry.texture_displacement[slot];
|
||||
if (mask.empty())
|
||||
continue; // written by a build without the feature
|
||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
||||
facets.reserve(triangles_count);
|
||||
for (size_t i = 0; i < triangles_count; ++i) {
|
||||
const size_t index = volume_data.first_triangle_id + i;
|
||||
if (index < mask.size() && !mask[index].empty())
|
||||
facets.set_triangle_from_string(i, mask[index]);
|
||||
}
|
||||
facets.shrink_to_fit();
|
||||
facets.touch();
|
||||
}
|
||||
volume->set_type(volume_data.part_type);
|
||||
|
||||
// apply the remaining volume's metadata
|
||||
for (const Metadata& metadata : volume_data.metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5805,6 +5906,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
||||
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
|
||||
mask.resize(current_object->geometry.triangles.size());
|
||||
mask.back() = paint;
|
||||
}
|
||||
// BBS
|
||||
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -6058,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)
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -6156,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;
|
||||
@@ -6366,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);
|
||||
@@ -6414,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6460,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))){
|
||||
@@ -6754,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;
|
||||
}
|
||||
@@ -6808,6 +6928,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
|
||||
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
|
||||
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
|
||||
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
|
||||
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
|
||||
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
|
||||
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
|
||||
stream << "</Types>";
|
||||
|
||||
@@ -6926,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";
|
||||
@@ -6937,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)
|
||||
@@ -6945,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)
|
||||
@@ -6970,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()) {
|
||||
@@ -7232,10 +7362,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (iter != m_shared_meshes.end())
|
||||
{
|
||||
const ModelVolume* shared_volume = iter->second.second;
|
||||
bool same_texture_paint = true;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
|
||||
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
|
||||
if ((shared_volume->supported_facets.equals(volume->supported_facets))
|
||||
&& (shared_volume->seam_facets.equals(volume->seam_facets))
|
||||
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
|
||||
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
|
||||
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
|
||||
&& same_texture_paint)
|
||||
{
|
||||
auto data = iter->second.first;
|
||||
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
|
||||
@@ -7277,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++;
|
||||
}
|
||||
|
||||
@@ -7651,6 +7785,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
|
||||
// not know, so a project written here still opens in a build without the feature - it just
|
||||
// loses the paint, which is also all it could have done with it.
|
||||
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
|
||||
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
|
||||
if (texture_paint.empty())
|
||||
continue;
|
||||
output_buffer += " ";
|
||||
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
|
||||
output_buffer += "=\"";
|
||||
output_buffer += texture_paint;
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// BBS
|
||||
if (i < its.properties.size()) {
|
||||
std::string prop_str = its.properties[i].to_string();
|
||||
@@ -7713,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";
|
||||
@@ -8100,7 +8247,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return file_path;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
|
||||
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
|
||||
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
|
||||
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
|
||||
// every reader has to parse just to list the objects.
|
||||
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
|
||||
const std::string &id)
|
||||
{
|
||||
if (volume.texture_displacement_layers.empty())
|
||||
return;
|
||||
|
||||
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
|
||||
// being exported is const (and may be saved again, elsewhere, with different paths).
|
||||
std::vector<TextureDisplacementLayer> layers = volume.texture_displacement_layers;
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
layer.path_in_3mf.clear();
|
||||
if (!layer.image_data || layer.image_data->empty())
|
||||
continue;
|
||||
// Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
|
||||
// greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
|
||||
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
|
||||
// No deflate: a PNG is already compressed, so a second pass only costs time.
|
||||
if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
|
||||
MZ_NO_COMPRESSION))
|
||||
layer.path_in_3mf = path;
|
||||
}
|
||||
|
||||
const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
|
||||
const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
|
||||
if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
|
||||
return;
|
||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
|
||||
<< VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||
{
|
||||
std::stringstream stream;
|
||||
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
||||
@@ -8116,7 +8298,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
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())
|
||||
@@ -8211,6 +8394,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
|
||||
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
|
||||
}
|
||||
|
||||
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
|
||||
|
||||
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
|
||||
tc.has_value())
|
||||
@@ -9591,7 +9776,10 @@ bool has_restore_data(std::string & path, std::string& origin)
|
||||
origin = "<lock>";
|
||||
return false;
|
||||
}
|
||||
if (boost::filesystem::exists(path + "/lock.txt")) {
|
||||
// ORCA: non-throwing probes; ec is deliberately unread, a backup path that cannot be
|
||||
// probed is treated as having no restore data rather than aborting the application.
|
||||
boost::system::error_code ec;
|
||||
if (boost::filesystem::exists(path + "/lock.txt", ec)) {
|
||||
std::string pid;
|
||||
load_string_file(path + "/lock.txt", pid);
|
||||
try {
|
||||
@@ -9606,10 +9794,10 @@ bool has_restore_data(std::string & path, std::string& origin)
|
||||
}
|
||||
}
|
||||
std::string file3mf = path + "/.3mf";
|
||||
if (!boost::filesystem::exists(file3mf))
|
||||
if (!boost::filesystem::exists(file3mf, ec))
|
||||
return false;
|
||||
try {
|
||||
if (boost::filesystem::exists(path + "/origin.txt"))
|
||||
if (boost::filesystem::exists(path + "/origin.txt", ec))
|
||||
load_string_file(path + "/origin.txt", origin);
|
||||
}
|
||||
catch (...) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+28
-11
@@ -1033,6 +1033,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return buf;
|
||||
}
|
||||
|
||||
// FanMover consumes these markers (and drops them) to avoid splitting a G1 inside custom gcode.
|
||||
// Only written when FanMover runs, the same gate as its construction in GCode::process_layers.
|
||||
static std::string wrap_custom_gcode_for_fan_mover(const FullPrintConfig &config, const std::string &gcode)
|
||||
{
|
||||
if (config.fan_speedup_time.value == 0 && config.fan_kickstart.value <= 0)
|
||||
return gcode;
|
||||
return ";_FAN_MOVER_SKIP_START\n" + gcode + ";_FAN_MOVER_SKIP_END\n";
|
||||
}
|
||||
|
||||
std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const
|
||||
{
|
||||
if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
|
||||
@@ -1384,6 +1393,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config);
|
||||
|
||||
check_add_eol(toolchange_gcode_str);
|
||||
// Type1 wipe tower path (every BBL printer): bracket the same way set_extruder() does,
|
||||
// so FanMover doesn't split a G1 inside this custom gcode either.
|
||||
toolchange_gcode_str = wrap_custom_gcode_for_fan_mover(gcodegen.config(), toolchange_gcode_str);
|
||||
|
||||
//BBS
|
||||
{
|
||||
@@ -1967,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.
|
||||
@@ -2006,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);
|
||||
@@ -2032,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;
|
||||
@@ -2780,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;
|
||||
}
|
||||
@@ -2806,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.
|
||||
@@ -2937,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;
|
||||
@@ -4149,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);
|
||||
}
|
||||
@@ -4250,7 +4266,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get())));
|
||||
config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get())));
|
||||
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
|
||||
std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.values.at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config);
|
||||
std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.get_at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config);
|
||||
file.writeln(filament_start_gcode);
|
||||
// Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where
|
||||
// H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their
|
||||
@@ -10895,7 +10911,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
|
||||
toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config);
|
||||
check_add_eol(toolchange_gcode_parsed);
|
||||
gcode += toolchange_gcode_parsed;
|
||||
// toolchange_gcode_parsed is still read below (get_last_z_from_gcode, custom_gcode_changes_tool).
|
||||
gcode += wrap_custom_gcode_for_fan_mover(m_config, toolchange_gcode_parsed);
|
||||
|
||||
//BBS
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user