Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67eee11e25 | ||
|
|
253f90c975 | ||
|
|
7b717ffa7d | ||
|
|
f42173a19f | ||
|
|
b99a9a06f3 | ||
|
|
faa64ab089 | ||
|
|
987b1b2b63 | ||
|
|
9bd4c71a19 | ||
|
|
3c646f305c | ||
|
|
432d6be518 | ||
|
|
3e7f0c027a | ||
|
|
504a143b98 | ||
|
|
a87639ac41 | ||
|
|
ecbd03fed7 | ||
|
|
ccbb73baaf | ||
|
|
029bc7928f | ||
|
|
0a8fce0913 | ||
|
|
3942ba7005 | ||
|
|
0110572a77 | ||
|
|
2c2869ab07 | ||
|
|
bf63eb1226 | ||
|
|
38e57888a5 | ||
|
|
fc079b2e5b | ||
|
|
a88843c6f8 |
@@ -1,220 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,64 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,102 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,113 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,870 +0,0 @@
|
||||
#!/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,11 +570,6 @@ 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`).
|
||||
@@ -612,32 +607,6 @@ 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
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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"
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/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
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
@@ -32,7 +31,6 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
@@ -443,10 +441,14 @@ 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: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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
|
||||
|
||||
@@ -33,8 +33,6 @@ jobs:
|
||||
ubuntu-ver: '2404'
|
||||
ubuntu-ver-str: '_Ubuntu2404'
|
||||
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
|
||||
NSIS_VERSION: '3.13'
|
||||
NSIS_SHA256: BA63DFFC4410EE89193E1CB5A41989991BD77C61068DA17E3156D136B7B0B3D8
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -343,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
|
||||
retry xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
|
||||
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
|
||||
retry xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
|
||||
xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
|
||||
fi
|
||||
|
||||
- name: Create DMG without notary
|
||||
@@ -399,18 +397,25 @@ 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: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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 && hashFiles(format('OrcaSlicer_profile_validator_Mac_universal_{0}.dmg', env.ver)) != ''
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
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
|
||||
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
|
||||
@@ -429,49 +434,36 @@ jobs:
|
||||
if: runner.os == 'Windows'
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
# NSIS builds the installer. A pinned portable copy is kept in the Actions
|
||||
# cache, so most runs never download it, and a cache miss fetches the
|
||||
# release archive from SourceForge instead of going through the Chocolatey
|
||||
# community feed, which hosted runners often cannot reach. Prepending it to
|
||||
# PATH also makes cpack use it over the NSIS preinstalled on some images.
|
||||
- name: Restore NSIS
|
||||
id: nsis-cache
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/nsis
|
||||
key: nsis-${{ env.NSIS_VERSION }}
|
||||
|
||||
- name: Install NSIS
|
||||
- name: Install nsis
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
shell: pwsh
|
||||
# The Chocolatey community feed intermittently 504s, and `choco install`
|
||||
# exits 0 when package resolution fails that way. Unchecked, the job then
|
||||
# builds for ~25 minutes before `cpack -G NSIS` reports a missing makensis.
|
||||
# Retry the install and verify makensis itself, so a real failure stops here.
|
||||
run: |
|
||||
dir "C:/Program Files (x86)/Windows Kits/10/Include"
|
||||
$nsisDir = Join-Path $env:RUNNER_TEMP 'nsis'
|
||||
$nsisDir = Join-Path ${env:ProgramFiles(x86)} 'NSIS'
|
||||
$makensis = Join-Path $nsisDir 'makensis.exe'
|
||||
for ($attempt = 1; $attempt -le 3 -and -not (Test-Path $makensis); $attempt++) {
|
||||
if ($attempt -gt 1) { Start-Sleep -Seconds (15 * $attempt) }
|
||||
Write-Host "::group::choco install nsis (attempt $attempt)"
|
||||
choco install nsis --yes --no-progress
|
||||
Write-Host "::endgroup::"
|
||||
}
|
||||
if (-not (Test-Path $makensis)) {
|
||||
$zip = Join-Path $env:RUNNER_TEMP "nsis-$env:NSIS_VERSION.zip"
|
||||
curl.exe -fsSL --retry 5 --retry-all-errors --retry-delay 15 -o $zip "https://downloads.sourceforge.net/project/nsis/NSIS%203/$env:NSIS_VERSION/nsis-$env:NSIS_VERSION.zip"
|
||||
if ($LASTEXITCODE -ne 0) { throw "NSIS $env:NSIS_VERSION download failed." }
|
||||
if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $env:NSIS_SHA256) {
|
||||
throw "NSIS $env:NSIS_VERSION archive does not match its pinned SHA256."
|
||||
}
|
||||
Expand-Archive $zip $env:RUNNER_TEMP
|
||||
Move-Item (Join-Path $env:RUNNER_TEMP "nsis-$env:NSIS_VERSION") $nsisDir
|
||||
throw "NSIS install failed: $makensis not found after 3 attempts."
|
||||
}
|
||||
& $makensis /VERSION
|
||||
$nsisDir | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
|
||||
|
||||
- name: Save NSIS
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED && steps.nsis-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/nsis
|
||||
key: nsis-${{ env.NSIS_VERSION }}
|
||||
|
||||
- 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.
|
||||
@@ -554,27 +546,36 @@ jobs:
|
||||
|
||||
- name: Deploy Windows release portable
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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
|
||||
content_type: application/x-zip-compressed
|
||||
asset_content_type: application/x-zip-compressed
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy Windows release installer
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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
|
||||
content_type: application/x-msdownload
|
||||
asset_content_type: application/x-msdownload
|
||||
max_releases: 1
|
||||
|
||||
- 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: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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
|
||||
content_type: application/x-msdownload
|
||||
asset_content_type: application/x-msdownload
|
||||
max_releases: 1
|
||||
|
||||
- name: Build MSIX Store package Win
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
@@ -703,10 +704,14 @@ jobs:
|
||||
|
||||
- name: Deploy Ubuntu release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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: |
|
||||
@@ -715,24 +720,36 @@ 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: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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: ./.github/actions/deploy-nightly
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
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,7 +13,6 @@ 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.
|
||||
@@ -60,21 +59,6 @@ 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 }}
|
||||
@@ -233,7 +217,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.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.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.
|
||||
@@ -250,17 +234,6 @@ 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 ""
|
||||
@@ -310,7 +283,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
- name: Fail if any check failed
|
||||
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') }}
|
||||
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') }}
|
||||
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,15 +16,6 @@ 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 }}
|
||||
@@ -44,7 +35,7 @@ jobs:
|
||||
# The PR merge commit plus its first parent, the base it is diffed against.
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Look for changed files
|
||||
- name: Look for changed C++ 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
|
||||
@@ -52,75 +43,18 @@ 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/.
|
||||
# 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.
|
||||
# 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
|
||||
id: deps_cache
|
||||
if: steps.changes.outputs.cpp == 'true'
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||
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 }}
|
||||
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
|
||||
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
|
||||
|
||||
@@ -67,7 +67,6 @@ 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
|
||||
@@ -75,19 +74,6 @@ 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,6 +497,47 @@ 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)
|
||||
@@ -1345,8 +1386,7 @@ set (CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
|
||||
set (CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/images\\\\OrcaSlicer.ico")
|
||||
set (CPACK_NSIS_MUI_ICON "${CPACK_PACKAGE_ICON}")
|
||||
set (CPACK_NSIS_MUI_UNIICON "${CPACK_PACKAGE_ICON}")
|
||||
# Relative to the install dir: CPack's NSIS template prefixes $INSTDIR itself.
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "orca-slicer.exe")
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "$INSTDIR\\\\orca-slicer.exe")
|
||||
set (CPACK_PACKAGE_CHECKSUM SHA256)
|
||||
set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "OrcaSlicer")
|
||||
set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
|
||||
|
||||
@@ -86,8 +86,7 @@ The [wiki](https://www.orcaslicer.com/wiki) aims to provide a detailed explanati
|
||||
## Stable Release
|
||||
|
||||
📥 **[Download the Latest Stable Release](https://github.com/OrcaSlicer/OrcaSlicer/releases/latest)**
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
Windows release binaries are code signed; see our [Code signing policy](#code-signing-policy).
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
|
||||
## Nightly Builds
|
||||
|
||||
@@ -245,16 +244,6 @@ OrcaSlicer began in that same spirit, drawing from BambuStudio, PrusaSlicer, and
|
||||
|
||||
The OrcaSlicer logo was designed by community member [Justin Levine](https://github.com/jal-co).
|
||||
|
||||
# Code Signing Policy
|
||||
|
||||
Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org).
|
||||
|
||||
Windows release binaries are built by GitHub Actions from this repository and signed through the SignPath Foundation program. Every signing request is manually approved.
|
||||
|
||||
**Team roles:** [SoftFever](https://github.com/SoftFever) is the committer, reviewer and approver for all signed releases.
|
||||
|
||||
**Privacy policy:** See the [OrcaSlicer privacy policy](https://www.orcaslicer.com/privacy-policy/).
|
||||
|
||||
# License
|
||||
|
||||
- **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3.
|
||||
|
||||
@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
|
||||
pushd "${SCRIPT_PATH}" > /dev/null
|
||||
|
||||
function usage() {
|
||||
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-J N][-p][-r][-s][-t][-u][-l][-L] [-- CMAKE_ARGS...]"
|
||||
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-J N][-p][-r][-s][-t][-u][-l][-L]"
|
||||
echo " -1: limit builds to one core (where possible)"
|
||||
echo " -j N: limit builds to N cores (where possible)"
|
||||
echo " -J N: build up to N dependencies at a time, each still using -j jobs (default: 1)"
|
||||
@@ -29,15 +29,11 @@ function usage() {
|
||||
echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
|
||||
echo " -l: use Clang instead of GCC (default: GCC)"
|
||||
echo " -L: use ld.lld as linker (if available)"
|
||||
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and OrcaSlicer (-s/-t) builds,"
|
||||
echo " e.g. -DCMAKE_CXX_FLAGS=\"-O2 -march=native\", -DCMAKE_CXX_COMPILER=g++-14, -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=mold."
|
||||
echo " They come last on the cmake command line, so they override what -l and -L set."
|
||||
echo "For a first use, you want to './${SCRIPT_NAME} -u'"
|
||||
echo " and then './${SCRIPT_NAME} -dsi'"
|
||||
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
|
||||
echo "Use './${SCRIPT_NAME} -gF -istrlL' to rebuild the cached runner image first."
|
||||
echo "Set ORCA_CONTAINER_CLI, ORCA_DOCKER_IMAGE, ORCA_DOCKER_BASE_IMAGE, or ORCA_DOCKER_CMAKE_VERSION to override the container runtime, cached image tag, base image, or CMake version."
|
||||
echo "DEPS_EXTRA_BUILD_ARGS and ORCA_EXTRA_BUILD_ARGS add whitespace-separated cmake configure arguments to the deps and OrcaSlicer builds; prefer '--' for arguments that contain spaces."
|
||||
}
|
||||
|
||||
SLIC3R_PRECOMPILED_HEADERS="ON"
|
||||
@@ -135,20 +131,6 @@ while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
|
||||
esac
|
||||
done
|
||||
|
||||
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
|
||||
if [[ ${OPTIND} -le $# ]] ; then
|
||||
separator_index=$((OPTIND-1))
|
||||
if [[ "${!separator_index}" != "--" ]] ; then
|
||||
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
shift $((OPTIND-1))
|
||||
EXTRA_CMAKE_ARGS=("$@")
|
||||
if [[ ${#EXTRA_CMAKE_ARGS[@]} -gt 0 ]] ; then
|
||||
FORWARDED_ARGS+=("--" "${EXTRA_CMAKE_ARGS[@]}")
|
||||
fi
|
||||
|
||||
if [ ${OPTIND} -eq 1 ] ; then
|
||||
usage
|
||||
exit 1
|
||||
@@ -565,7 +547,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
|
||||
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
|
||||
fi
|
||||
|
||||
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}" "${EXTRA_CMAKE_ARGS[@]}"
|
||||
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
|
||||
# The top-level build runs one dependency at a time by default, which keeps the console
|
||||
# output readable and lets that dependency's own build use all of CMAKE_BUILD_PARALLEL_LEVEL.
|
||||
# -J raises the top level instead, and -j still applies in full to every dependency, so the
|
||||
@@ -611,8 +593,7 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
|
||||
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
|
||||
-DORCA_TOOLS=ON \
|
||||
"${COLORED_OUTPUT}" \
|
||||
"${BUILD_ARGS[@]}" \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
"${BUILD_ARGS[@]}"
|
||||
echo "done"
|
||||
if [[ -n "${BUILD_ORCA}" ]]; then
|
||||
echo "Building OrcaSlicer ..."
|
||||
|
||||
@@ -42,7 +42,6 @@ call :add_arg build_deps bool d deps "Download and build the dependencies, neede
|
||||
call :add_arg build_slicer bool s slicer "Build OrcaSlicer"
|
||||
call :add_arg build_tests bool "" tests "Build the unit tests"
|
||||
call :add_arg run_tests bool "" run-tests "Build the unit tests and run them"
|
||||
call :add_arg make_installer bool "" installer "Build OrcaSlicer and package it into an NSIS installer"
|
||||
call :add_arg pack_deps bool p pack "Bundle the built dependencies into a zip file"
|
||||
call :add_arg install_deps bool u install-deps "Install or update CMake, Perl and Git with WinGet"
|
||||
call :add_arg install_vs string "" install-vs "Also install Visual Studio: buildtools or ide"
|
||||
@@ -186,9 +185,6 @@ if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" (
|
||||
if "%run_tests%" == "ON" set "build_slicer=ON"
|
||||
)
|
||||
|
||||
REM The installer packages the slicer build, so it always needs one.
|
||||
if "%make_installer%" == "ON" set "build_slicer=ON"
|
||||
|
||||
REM Options like --config or -j only shape a build. Without one of the actions
|
||||
REM there is nothing for them to shape, so say so rather than resolving a whole
|
||||
REM build and reporting it took no time. The block above may have added one.
|
||||
@@ -226,30 +222,6 @@ if "%build_slicer%" == "ON" if "%install_slicer%" == "ON" if not "%slicer_target
|
||||
echo --install needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
REM cpack runs the same install, so it has the same need.
|
||||
if "%make_installer%" == "ON" if not "%slicer_target%" == "" if /I not "%slicer_target%" == "OrcaSlicer" (
|
||||
echo --installer needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM cpack finds makensis on PATH or through the registry key the NSIS installer
|
||||
REM writes, so look in the same places, and before the build rather than after
|
||||
REM it. The value is read past its REG_SZ marker because the name column is
|
||||
REM localised and may hold spaces.
|
||||
if not "%make_installer%" == "ON" goto :nsis_ready
|
||||
if "%dry_run%" == "ON" goto :nsis_ready
|
||||
where makensis >nul 2>nul && goto :nsis_ready
|
||||
for %%v in (32 64) do (
|
||||
for /f "delims=" %%l in ('reg query "HKLM\SOFTWARE\NSIS" /ve /reg:%%v 2^>nul ^| findstr REG_SZ') do (
|
||||
set "nsis_line=%%l"
|
||||
set "nsis_line=!nsis_line:*REG_SZ=!"
|
||||
for /f "tokens=*" %%d in ("!nsis_line!") do if exist "%%d\makensis.exe" goto :nsis_ready
|
||||
)
|
||||
)
|
||||
echo NSIS was not found, and --installer needs it to build the installer.
|
||||
echo Install it with "winget install NSIS.NSIS", then run this again.
|
||||
exit /b 1
|
||||
:nsis_ready
|
||||
if not "%vs_version%" == "" if "!vs_gen_%vs_version%!" == "" (
|
||||
echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026.
|
||||
exit /b 1
|
||||
@@ -762,14 +734,6 @@ if "%build_slicer%" == "ON" (
|
||||
call :print_and_run cmake --build "%build_dir%" --target install --config %build_type%
|
||||
%error_check%
|
||||
)
|
||||
|
||||
REM cpack installs into its own staging tree, so this does not need -i.
|
||||
REM Unlike CI it skips the preset cache, whose packaging step prunes the
|
||||
REM checkout's preset JSONs.
|
||||
if "%make_installer%" == "ON" (
|
||||
call :print_and_run cpack -G NSIS -C %build_type% --config "%build_dir%\CPackConfig.cmake" -B "%build_dir%"
|
||||
%error_check%
|
||||
)
|
||||
)
|
||||
|
||||
REM Elapsed wall clock. The 1%%a-100 trick strips a leading zero, which set /A
|
||||
@@ -856,16 +820,6 @@ REM worked out the same way in either run.
|
||||
if "%build_slicer%" == "ON" if not "%linked%" == "ON" echo Target %slicer_target%
|
||||
if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution!
|
||||
if "%pack_deps%" == "ON" if defined bundle echo Bundle !bundle!
|
||||
if "%make_installer%" == "ON" (
|
||||
REM The file name carries the version, which only CMake knows, so name
|
||||
REM the newest match, or the pattern when there is none yet.
|
||||
set "installer="
|
||||
for /f "delims=" %%f in ('dir /b /o-d "!build_full!\OrcaSlicer_Windows_Installer_*.exe" 2^>nul') do (
|
||||
if not defined installer set "installer=!build_full!\%%f"
|
||||
)
|
||||
if not defined installer set "installer=!build_full!\OrcaSlicer_Windows_Installer_*.exe"
|
||||
echo Installer !installer!
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Next
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
# 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 ()
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Find the wxWidgets module based on the information provided by the Perl Alien::wxWidgets module.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(AlienWx Perl)
|
||||
libfind_package(AlienWx PerlLibs)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_FIND_COMPONENTS=${AlienWx_FIND_COMPONENTS}")
|
||||
endif()
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(AlienWx_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/AlienWx_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Text::ParseWords;
|
||||
|
||||
BEGIN {
|
||||
# CMake sets the environment variables CC and CXX to the detected C compiler.
|
||||
# There is an issue with the Perl ExtUtils::CBuilder, which does not handle whitespaces
|
||||
# in the paths correctly on Windows, so we rather drop the CMake auto-detected paths.
|
||||
delete \$ENV{CC};
|
||||
delete \$ENV{CXX};
|
||||
}
|
||||
|
||||
use Alien::wxWidgets;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# List of wxWidgets components to be used.
|
||||
my @components = split /;/, '${AlienWx_FIND_COMPONENTS}';
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$version = Alien::wxWidgets->version;
|
||||
my \$config = Alien::wxWidgets->config;
|
||||
my \$compiler = Alien::wxWidgets->compiler;
|
||||
my \$linker = Alien::wxWidgets->linker;
|
||||
my \$include_path = ' ' . Alien::wxWidgets->include_path;
|
||||
my \$defines = ' ' . Alien::wxWidgets->defines;
|
||||
my \$cflags = Alien::wxWidgets->c_flags;
|
||||
my \$linkflags = Alien::wxWidgets->link_flags;
|
||||
my \$libraries = ' ' . Alien::wxWidgets->libraries(@components);
|
||||
my \$gui_toolkit = Alien::wxWidgets->config->{toolkit};
|
||||
#my @libraries = Alien::wxWidgets->link_libraries(@components);
|
||||
#my @implib = Alien::wxWidgets->import_libraries(@components);
|
||||
#my @shrlib = Alien::wxWidgets->shared_libraries(@components);
|
||||
#my @keys = Alien::wxWidgets->library_keys; # 'gl', 'adv', ...
|
||||
#my \$library_path = Alien::wxWidgets->shared_library_path;
|
||||
#my \$key = Alien::wxWidgets->key;
|
||||
#my \$prefix = Alien::wxWidgets->prefix;
|
||||
|
||||
my \$filename = '${AlienWx_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(AlienWx_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('VERSION', \$version);
|
||||
\$include_path =~ s/ -I/ /g;
|
||||
cmake_set_var('INCLUDE_DIRS', \$include_path);
|
||||
\$libraries =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LIBRARIES', \$libraries);
|
||||
#cmake_set_var('LIBRARY_DIRS', );
|
||||
#\$defines =~ s/ -D/ /g;
|
||||
cmake_set_var('DEFINITIONS', \$defines);
|
||||
#cmake_set_var('DEFINITIONS_DEBUG', );
|
||||
cmake_set_var('CXX_FLAGS', \$cflags);
|
||||
cmake_set_var('GUI_TOOLKIT', \$gui_toolkit);
|
||||
close \$fh;
|
||||
")
|
||||
include(${AlienWx_TEMP_INCLUDE})
|
||||
file(REMOVE ${AlienWx_TEMP_INCLUDE})
|
||||
unset(AlienWx_TEMP_INCLUDE)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_VERSION = ${AlienWx_VERSION}")
|
||||
message(STATUS " AlienWx_INCLUDE_DIRS = ${AlienWx_INCLUDE_DIRS}")
|
||||
message(STATUS " AlienWx_LIBRARIES = ${AlienWx_LIBRARIES}")
|
||||
message(STATUS " AlienWx_LIBRARY_DIRS = ${AlienWx_LIBRARY_DIRS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS = ${AlienWx_DEFINITIONS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS_DEBUG = ${AlienWx_DEFINITIONS_DEBUG}")
|
||||
message(STATUS " AlienWx_CXX_FLAGS = ${AlienWx_CXX_FLAGS}")
|
||||
message(STATUS " AlienWx_GUI_TOOLKIT = ${AlienWx_GUI_TOOLKIT}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(AlienWx
|
||||
REQUIRED_VARS AlienWx_INCLUDE_DIRS AlienWx_LIBRARIES
|
||||
# HANDLE_COMPONENTS
|
||||
VERSION_VAR AlienWx_VERSION)
|
||||
@@ -0,0 +1,88 @@
|
||||
# Find the dependencies for linking with the Perl runtime library.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(PerlEmbed Perl)
|
||||
libfind_package(PerlEmbed PerlLibs)
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(PerlEmbed_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/PerlEmbed_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -MExtUtils::Embed -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Config;
|
||||
use Text::ParseWords;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$ccflags;
|
||||
my \$ldflags;
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ccflags; ccflags; }
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ldflags; ldopts; }
|
||||
\$ccflags = ' ' . \$ccflags;
|
||||
\$ldflags = ' ' . \$ldflags;
|
||||
|
||||
my \$filename = '${PerlEmbed_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(PerlEmbed_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('ARCHNAME', \$Config{archname});
|
||||
cmake_set_var('CCFLAGS', \$ccflags);
|
||||
\$ldflags =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LD', \$Config{ld});
|
||||
cmake_set_var('LDFLAGS', \$ldflags);
|
||||
cmake_set_var('CCCDLFLAGS', \$Config{cccdlflags});
|
||||
cmake_set_var('LDDLFLAGS', \$Config{lddlflags});
|
||||
cmake_set_var('DLEXT', \$Config{dlext});
|
||||
close \$fh;
|
||||
")
|
||||
include(${PerlEmbed_TEMP_INCLUDE})
|
||||
file(REMOVE ${PerlEmbed_TEMP_INCLUDE})
|
||||
unset(PerlEmbed_TEMP_INCLUDE)
|
||||
|
||||
if (PerlEmbed_DEBUG)
|
||||
# First show the configuration extracted by FindPerl & FindPerlLibs:
|
||||
message(STATUS " PERL_INCLUDE_PATH = ${PERL_INCLUDE_PATH}")
|
||||
message(STATUS " PERL_LIBRARY = ${PERL_LIBRARY}")
|
||||
message(STATUS " PERL_EXECUTABLE = ${PERL_EXECUTABLE}")
|
||||
message(STATUS " PERL_SITESEARCH = ${PERL_SITESEARCH}")
|
||||
message(STATUS " PERL_SITELIB = ${PERL_SITELIB}")
|
||||
message(STATUS " PERL_VENDORARCH = ${PERL_VENDORARCH}")
|
||||
message(STATUS " PERL_VENDORLIB = ${PERL_VENDORLIB}")
|
||||
message(STATUS " PERL_ARCHLIB = ${PERL_ARCHLIB}")
|
||||
message(STATUS " PERL_PRIVLIB = ${PERL_PRIVLIB}")
|
||||
message(STATUS " PERL_EXTRA_C_FLAGS = ${PERL_EXTRA_C_FLAGS}")
|
||||
# Second show the configuration extracted by this module (FindPerlEmbed):
|
||||
message(STATUS " PerlEmbed_ARCHNAME = ${PerlEmbed_ARCHNAME}")
|
||||
message(STATUS " PerlEmbed_CCFLAGS = ${PerlEmbed_CCFLAGS}")
|
||||
message(STATUS " PerlEmbed_CCCDLFLAGS = ${PerlEmbed_CCCDLFLAGS}")
|
||||
message(STATUS " LD = ${PerlEmbed_LD}")
|
||||
message(STATUS " PerlEmbed_LDFLAGS = ${PerlEmbed_LDFLAGS}")
|
||||
message(STATUS " PerlEmbed_LDDLFLAGS = ${PerlEmbed_LDDLFLAGS}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(PerlEmbed
|
||||
REQUIRED_VARS PerlEmbed_CCFLAGS PerlEmbed_LDFLAGS
|
||||
VERSION_VAR PERL_VERSION)
|
||||
@@ -0,0 +1,265 @@
|
||||
# Version 2.2
|
||||
# Public Domain, originally written by Lasse Kärkkäinen <tronic>
|
||||
# Maintained at https://github.com/Tronic/cmake-modules
|
||||
# Please send your improvements as pull requests on Github.
|
||||
|
||||
# Find another package and make it a dependency of the current package.
|
||||
# This also automatically forwards the "REQUIRED" argument.
|
||||
# Usage: libfind_package(<prefix> <another package> [extra args to find_package])
|
||||
macro (libfind_package PREFIX PKG)
|
||||
set(${PREFIX}_args ${PKG} ${ARGN})
|
||||
if (${PREFIX}_FIND_REQUIRED)
|
||||
set(${PREFIX}_args ${${PREFIX}_args} REQUIRED)
|
||||
endif()
|
||||
find_package(${${PREFIX}_args})
|
||||
set(${PREFIX}_DEPENDENCIES ${${PREFIX}_DEPENDENCIES};${PKG})
|
||||
unset(${PREFIX}_args)
|
||||
endmacro()
|
||||
|
||||
# A simple wrapper to make pkg-config searches a bit easier.
|
||||
# Works the same as CMake's internal pkg_check_modules but is always quiet.
|
||||
macro (libfind_pkg_check_modules)
|
||||
find_package(PkgConfig QUIET)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(${ARGN} QUIET)
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Avoid useless copy&pasta by doing what most simple libraries do anyway:
|
||||
# pkg-config, find headers, find library.
|
||||
# Usage: libfind_pkg_detect(<prefix> <pkg-config args> FIND_PATH <name> [other args] FIND_LIBRARY <name> [other args])
|
||||
# E.g. libfind_pkg_detect(SDL2 sdl2 FIND_PATH SDL.h PATH_SUFFIXES SDL2 FIND_LIBRARY SDL2)
|
||||
function (libfind_pkg_detect PREFIX)
|
||||
# Parse arguments
|
||||
set(argname pkgargs)
|
||||
foreach (i ${ARGN})
|
||||
if ("${i}" STREQUAL "FIND_PATH")
|
||||
set(argname pathargs)
|
||||
elseif ("${i}" STREQUAL "FIND_LIBRARY")
|
||||
set(argname libraryargs)
|
||||
else()
|
||||
set(${argname} ${${argname}} ${i})
|
||||
endif()
|
||||
endforeach()
|
||||
if (NOT pkgargs)
|
||||
message(FATAL_ERROR "libfind_pkg_detect requires at least a pkg_config package name to be passed.")
|
||||
endif()
|
||||
# Find library
|
||||
libfind_pkg_check_modules(${PREFIX}_PKGCONF ${pkgargs})
|
||||
if (pathargs)
|
||||
find_path(${PREFIX}_INCLUDE_DIR NAMES ${pathargs} HINTS ${${PREFIX}_PKGCONF_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (libraryargs)
|
||||
find_library(${PREFIX}_LIBRARY NAMES ${libraryargs} HINTS ${${PREFIX}_PKGCONF_LIBRARY_DIRS})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Extracts a version #define from a version.h file, output stored to <PREFIX>_VERSION.
|
||||
# Usage: libfind_version_header(Foobar foobar/version.h FOOBAR_VERSION_STR)
|
||||
# Fourth argument "QUIET" may be used for silently testing different define names.
|
||||
# This function does nothing if the version variable is already defined.
|
||||
function (libfind_version_header PREFIX VERSION_H DEFINE_NAME)
|
||||
# Skip processing if we already have a version or if the include dir was not found
|
||||
if (${PREFIX}_VERSION OR NOT ${PREFIX}_INCLUDE_DIR)
|
||||
return()
|
||||
endif()
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
# Process optional arguments
|
||||
foreach(arg ${ARGN})
|
||||
if (arg STREQUAL "QUIET")
|
||||
set(quiet TRUE)
|
||||
else()
|
||||
message(AUTHOR_WARNING "Unknown argument ${arg} to libfind_version_header ignored.")
|
||||
endif()
|
||||
endforeach()
|
||||
# Read the header and parse for version number
|
||||
set(filename "${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
if (NOT EXISTS ${filename})
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
file(READ "${filename}" header)
|
||||
string(REGEX REPLACE ".*#[ \t]*define[ \t]*${DEFINE_NAME}[ \t]*\"([^\n]*)\".*" "\\1" match "${header}")
|
||||
# No regex match?
|
||||
if (match STREQUAL header)
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find \#define ${DEFINE_NAME} \"<version>\" from ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
# Export the version string
|
||||
set(${PREFIX}_VERSION "${match}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Do the final processing once the paths have been detected.
|
||||
# If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain
|
||||
# all the variables, each of which contain one include directory.
|
||||
# Ditto for ${PREFIX}_PROCESS_LIBS and library files.
|
||||
# Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES.
|
||||
# Also handles errors in case library detection was required, etc.
|
||||
function (libfind_process PREFIX)
|
||||
# Skip processing if already processed during this configuration run
|
||||
if (${PREFIX}_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(found TRUE) # Start with the assumption that the package was found
|
||||
|
||||
# Did we find any files? Did we miss includes? These are for formatting better error messages.
|
||||
set(some_files FALSE)
|
||||
set(missing_headers FALSE)
|
||||
|
||||
# Shorthands for some variables that we need often
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
set(required ${${PREFIX}_FIND_REQUIRED})
|
||||
set(exactver ${${PREFIX}_FIND_VERSION_EXACT})
|
||||
set(findver "${${PREFIX}_FIND_VERSION}")
|
||||
set(version "${${PREFIX}_VERSION}")
|
||||
|
||||
# Lists of config option names (all, includes, libs)
|
||||
unset(configopts)
|
||||
set(includeopts ${${PREFIX}_PROCESS_INCLUDES})
|
||||
set(libraryopts ${${PREFIX}_PROCESS_LIBS})
|
||||
|
||||
# Process deps to add to
|
||||
foreach (i ${PREFIX} ${${PREFIX}_DEPENDENCIES})
|
||||
if (DEFINED ${i}_INCLUDE_OPTS OR DEFINED ${i}_LIBRARY_OPTS)
|
||||
# The package seems to export option lists that we can use, woohoo!
|
||||
list(APPEND includeopts ${${i}_INCLUDE_OPTS})
|
||||
list(APPEND libraryopts ${${i}_LIBRARY_OPTS})
|
||||
else()
|
||||
# If plural forms don't exist or they equal singular forms
|
||||
if ((NOT DEFINED ${i}_INCLUDE_DIRS AND NOT DEFINED ${i}_LIBRARIES) OR
|
||||
({i}_INCLUDE_DIR STREQUAL ${i}_INCLUDE_DIRS AND ${i}_LIBRARY STREQUAL ${i}_LIBRARIES))
|
||||
# Singular forms can be used
|
||||
if (DEFINED ${i}_INCLUDE_DIR)
|
||||
list(APPEND includeopts ${i}_INCLUDE_DIR)
|
||||
endif()
|
||||
if (DEFINED ${i}_LIBRARY)
|
||||
list(APPEND libraryopts ${i}_LIBRARY)
|
||||
endif()
|
||||
else()
|
||||
# Oh no, we don't know the option names
|
||||
message(FATAL_ERROR "We couldn't determine config variable names for ${i} includes and libs. Aieeh!")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (includeopts)
|
||||
list(REMOVE_DUPLICATES includeopts)
|
||||
endif()
|
||||
|
||||
if (libraryopts)
|
||||
list(REMOVE_DUPLICATES libraryopts)
|
||||
endif()
|
||||
|
||||
string(REGEX REPLACE ".*[ ;]([^ ;]*(_INCLUDE_DIRS|_LIBRARIES))" "\\1" tmp "${includeopts} ${libraryopts}")
|
||||
if (NOT tmp STREQUAL "${includeopts} ${libraryopts}")
|
||||
message(AUTHOR_WARNING "Plural form ${tmp} found in config options of ${PREFIX}. This works as before but is now deprecated. Please only use singular forms INCLUDE_DIR and LIBRARY, and update your find scripts for LibFindMacros > 2.0 automatic dependency system (most often you can simply remove the PROCESS variables entirely).")
|
||||
endif()
|
||||
|
||||
# Include/library names separated by spaces (notice: not CMake lists)
|
||||
unset(includes)
|
||||
unset(libs)
|
||||
|
||||
# Process all includes and set found false if any are missing
|
||||
foreach (i ${includeopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND includes "${${i}}")
|
||||
else()
|
||||
set(found FALSE)
|
||||
set(missing_headers TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Process all libraries and set found false if any are missing
|
||||
foreach (i ${libraryopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND libs "${${i}}")
|
||||
else()
|
||||
set (found FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Version checks
|
||||
if (found AND findver)
|
||||
if (NOT version)
|
||||
message(WARNING "The find module for ${PREFIX} does not provide version information, so we'll just assume that it is OK. Please fix the module or remove package version requirements to get rid of this warning.")
|
||||
elseif (version VERSION_LESS findver OR (exactver AND NOT version VERSION_EQUAL findver))
|
||||
set(found FALSE)
|
||||
set(version_unsuitable TRUE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# If all-OK, hide all config options, export variables, print status and exit
|
||||
if (found)
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(${i})
|
||||
endforeach()
|
||||
if (NOT quiet)
|
||||
message(STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}")
|
||||
if (LIBFIND_DEBUG)
|
||||
message(STATUS " ${PREFIX}_DEPENDENCIES=${${PREFIX}_DEPENDENCIES}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_OPTS=${includeopts}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_DIRS=${includes}")
|
||||
message(STATUS " ${PREFIX}_LIBRARY_OPTS=${libraryopts}")
|
||||
message(STATUS " ${PREFIX}_LIBRARIES=${libs}")
|
||||
endif()
|
||||
set (${PREFIX}_INCLUDE_OPTS ${includeopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARY_OPTS ${libraryopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_INCLUDE_DIRS ${includes} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARIES ${libs} PARENT_SCOPE)
|
||||
set (${PREFIX}_FOUND TRUE PARENT_SCOPE)
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Format messages for debug info and the type of error
|
||||
set(vars "Relevant CMake configuration variables:\n")
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(CLEAR ${i})
|
||||
set(val ${${i}})
|
||||
if ("${val}" STREQUAL "${i}-NOTFOUND")
|
||||
set (val "<not found>")
|
||||
elseif (val AND NOT EXISTS ${val})
|
||||
set (val "${val} (does not exist)")
|
||||
else()
|
||||
set(some_files TRUE)
|
||||
endif()
|
||||
set(vars "${vars} ${i}=${val}\n")
|
||||
endforeach()
|
||||
set(vars "${vars}You may use CMake GUI, cmake -D or ccmake to modify the values. Delete CMakeCache.txt to discard all values and force full re-detection if necessary.\n")
|
||||
if (version_unsuitable)
|
||||
set(msg "${PREFIX} ${${PREFIX}_VERSION} was found but")
|
||||
if (exactver)
|
||||
set(msg "${msg} only version ${findver} is acceptable.")
|
||||
else()
|
||||
set(msg "${msg} version ${findver} is the minimum requirement.")
|
||||
endif()
|
||||
else()
|
||||
if (missing_headers)
|
||||
set(msg "We could not find development headers for ${PREFIX}. Do you have the necessary dev package installed?")
|
||||
elseif (some_files)
|
||||
set(msg "We only found some files of ${PREFIX}, not all of them. Perhaps your installation is incomplete or maybe we just didn't look in the right place?")
|
||||
if(findver)
|
||||
set(msg "${msg} This could also be caused by incompatible version (if it helps, at least ${PREFIX} ${findver} should work).")
|
||||
endif()
|
||||
else()
|
||||
set(msg "We were unable to find package ${PREFIX}.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Fatal error out if REQUIRED
|
||||
if (required)
|
||||
set(msg "REQUIRED PACKAGE NOT FOUND\n${msg} This package is REQUIRED and you need to install it or adjust CMake configuration in order to continue building ${CMAKE_PROJECT_NAME}.")
|
||||
message(FATAL_ERROR "${msg}\n${vars}")
|
||||
endif()
|
||||
# Otherwise just print a nasty warning
|
||||
if (NOT quiet)
|
||||
message(WARNING "WARNING: MISSING PACKAGE\n${msg} This package is NOT REQUIRED and you may ignore this warning but by doing so you may miss some functionality of ${CMAKE_PROJECT_NAME}. \n${vars}")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -131,6 +131,13 @@
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>OrcaSlicer uses your local network to discover and connect to printers.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_octoprint._tcp</string>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<!-- Disable App Transport Security. Resolves https://github.com/OrcaSlicer/OrcaSlicer/issues/791 -->
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Source: https://gist.github.com/sivachandran/3a0de157dccef822a230#file-bin2h-cmake
|
||||
# Added modifications to suit prusaslicer
|
||||
include(CMakeParseArguments)
|
||||
|
||||
# Function to wrap a given string into multiple lines at the given column position.
|
||||
# Parameters:
|
||||
# VARIABLE - The name of the CMake variable holding the string.
|
||||
# AT_COLUMN - The column position at which string will be wrapped.
|
||||
function(WRAP_STRING)
|
||||
set(oneValueArgs VARIABLE AT_COLUMN)
|
||||
cmake_parse_arguments(WRAP_STRING "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
string(LENGTH ${${WRAP_STRING_VARIABLE}} stringLength)
|
||||
math(EXPR offset "0")
|
||||
|
||||
while(stringLength GREATER 0)
|
||||
|
||||
if(stringLength GREATER ${WRAP_STRING_AT_COLUMN})
|
||||
math(EXPR length "${WRAP_STRING_AT_COLUMN}")
|
||||
else()
|
||||
math(EXPR length "${stringLength}")
|
||||
endif()
|
||||
|
||||
string(SUBSTRING ${${WRAP_STRING_VARIABLE}} ${offset} ${length} line)
|
||||
set(lines "${lines}\n${line}")
|
||||
|
||||
math(EXPR stringLength "${stringLength} - ${length}")
|
||||
math(EXPR offset "${offset} + ${length}")
|
||||
endwhile()
|
||||
|
||||
set(${WRAP_STRING_VARIABLE} "${lines}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Function to embed contents of a file as byte array in C/C++ header file(.h). The header file
|
||||
# will contain a byte array and integer variable holding the size of the array.
|
||||
# Parameters
|
||||
# SOURCE_FILE - The path of source file whose contents will be embedded in the header file.
|
||||
# VARIABLE_NAME - The name of the variable for the byte array. The string "_SIZE" will be append
|
||||
# to this name and will be used a variable name for size variable.
|
||||
# HEADER_FILE - The path of header file.
|
||||
# APPEND - If specified appends to the header file instead of overwriting it
|
||||
# NULL_TERMINATE - If specified a null byte(zero) will be append to the byte array. This will be
|
||||
# useful if the source file is a text file and we want to use the file contents
|
||||
# as string. But the size variable holds size of the byte array without this
|
||||
# null byte.
|
||||
# Usage:
|
||||
# bin2h(SOURCE_FILE "Logo.png" HEADER_FILE "Logo.h" VARIABLE_NAME "LOGO_PNG")
|
||||
function(BIN2H)
|
||||
set(options APPEND NULL_TERMINATE ADD_WARNING_TEXT)
|
||||
set(oneValueArgs SOURCE_FILE VARIABLE_NAME HEADER_FILE)
|
||||
cmake_parse_arguments(BIN2H "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
# reads source file contents as hex string
|
||||
file(READ ${BIN2H_SOURCE_FILE} hexString HEX)
|
||||
string(LENGTH ${hexString} hexStringLength)
|
||||
|
||||
# appends null byte if asked
|
||||
if(BIN2H_NULL_TERMINATE)
|
||||
set(hexString "${hexString}00")
|
||||
endif()
|
||||
|
||||
# wraps the hex string into multiple lines at column 32(i.e. 16 bytes per line)
|
||||
wrap_string(VARIABLE hexString AT_COLUMN 32)
|
||||
math(EXPR arraySize "${hexStringLength} / 2")
|
||||
|
||||
# adds '0x' prefix and comma suffix before and after every byte respectively
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " arrayValues ${hexString})
|
||||
# removes trailing comma
|
||||
string(REGEX REPLACE ", $" "" arrayValues ${arrayValues})
|
||||
|
||||
# converts the variable name into proper C identifier
|
||||
string(MAKE_C_IDENTIFIER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
# string(TOUPPER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
|
||||
# declares byte array and the length variables
|
||||
set(arrayDefinition "const unsigned char ${BIN2H_VARIABLE_NAME}[] = { ${arrayValues} };")
|
||||
set(arraySizeDefinition "const size_t ${BIN2H_VARIABLE_NAME}_SIZE = ${arraySize};")
|
||||
set(warnTxt "")
|
||||
if (BIN2H_ADD_WARNING_TEXT)
|
||||
set(warnTxt "/* WARN: This file is auto-generated from ${BIN2H_SOURCE_FILE} */\n")
|
||||
endif ()
|
||||
|
||||
set(declarations "${warnTxt}${arrayDefinition}\n\n${arraySizeDefinition}\n\n")
|
||||
if(BIN2H_APPEND)
|
||||
file(APPEND ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
else()
|
||||
file(WRITE ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -0,0 +1,71 @@
|
||||
From b32e63f75046d186d93ee7d627c978f2f7c892f3 Mon Sep 17 00:00:00 2001
|
||||
From: Steve Dower <steve.dower@python.org>
|
||||
Date: Fri, 10 Jul 2026 14:04:00 +0100
|
||||
Subject: [PATCH] [3.13] gh-153438: Update NuGet download URL (GH-153482)
|
||||
(GH-153515)
|
||||
|
||||
gh-153438: Update NuGet download URL (GH-153482)
|
||||
(cherry picked from commit 106eb532ea3b243423e62a702719e9d3c0e40c16)
|
||||
(cherry picked from commit 62eb50d65d538dda5164f9c4a097a896454a0d1c)
|
||||
|
||||
Co-authored-by: Steve Dower <steve.dower@python.org>
|
||||
Co-authored-by: Harjoth Khara <harjoth.khara@gmail.com>
|
||||
---
|
||||
Doc/using/windows.rst | 6 +++---
|
||||
.../Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst | 2 ++
|
||||
PCbuild/find_python.bat | 2 +-
|
||||
Tools/msi/get_externals.bat | 2 +-
|
||||
4 files changed, 7 insertions(+), 5 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
|
||||
diff --git a/Doc/using/windows.rst b/Doc/using/windows.rst
|
||||
index 2a507675044666..4800789ca2d6cc 100644
|
||||
--- a/Doc/using/windows.rst
|
||||
+++ b/Doc/using/windows.rst
|
||||
@@ -414,9 +414,9 @@ on using nuget. What follows is a summary that is sufficient for Python
|
||||
developers.
|
||||
|
||||
The ``nuget.exe`` command line tool may be downloaded directly from
|
||||
-``https://aka.ms/nugetclidl``, for example, using curl or PowerShell. With the
|
||||
-tool, the latest version of Python for 64-bit or 32-bit machines is installed
|
||||
-using::
|
||||
+``https://dist.nuget.org/win-x86-commandline/latest/nuget.exe``, for example,
|
||||
+using curl or PowerShell. With the tool, the latest version of Python for
|
||||
+64-bit or 32-bit machines is installed using::
|
||||
|
||||
nuget.exe install python -ExcludeVersion -OutputDirectory .
|
||||
nuget.exe install pythonx86 -ExcludeVersion -OutputDirectory .
|
||||
diff --git a/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
new file mode 100644
|
||||
index 00000000000000..edab8e6ba7b259
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+Update Windows build and installer tooling and documentation to use the
|
||||
+current download URL for ``nuget.exe``.
|
||||
diff --git a/PCbuild/find_python.bat b/PCbuild/find_python.bat
|
||||
index 0af367a3efafad..9612a2860ef9c3 100644
|
||||
--- a/PCbuild/find_python.bat
|
||||
+++ b/PCbuild/find_python.bat
|
||||
@@ -52,7 +52,7 @@
|
||||
@set _Py_HOST_PYTHON=%HOST_PYTHON%
|
||||
@if "%_Py_HOST_PYTHON%"=="" set _Py_HOST_PYTHON=py
|
||||
@if "%_Py_NUGET%"=="" (set _Py_NUGET=%_Py_EXTERNALS_DIR%\nuget.exe)
|
||||
-@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
@if NOT exist "%_Py_NUGET%" (
|
||||
@if not "%_Py_Quiet%"=="1" @echo Downloading nuget...
|
||||
@rem NB: Must use single quotes around NUGET here, NOT double!
|
||||
diff --git a/Tools/msi/get_externals.bat b/Tools/msi/get_externals.bat
|
||||
index f6602ce9588ff4..c7c7e9f470bc25 100644
|
||||
--- a/Tools/msi/get_externals.bat
|
||||
+++ b/Tools/msi/get_externals.bat
|
||||
@@ -6,7 +6,7 @@ set HERE=%~dp0
|
||||
if "%PCBUILD%"=="" (set PCBUILD=%HERE%..\..\PCbuild\)
|
||||
if "%EXTERNALS_DIR%"=="" (set EXTERNALS_DIR=%HERE%..\..\externals\windows-installer)
|
||||
if "%NUGET%"=="" (set NUGET=%EXTERNALS_DIR%\..\nuget.exe)
|
||||
-if "%NUGET_URL%"=="" (set NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+if "%NUGET_URL%"=="" (set NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
|
||||
set DO_FETCH=true
|
||||
set DO_CLEAN=false
|
||||
@@ -8,45 +8,20 @@ set(_python_url "https://www.python.org/ftp/python/${_python_version}/Python-${_
|
||||
set(_python_sha256 "c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684")
|
||||
|
||||
|
||||
set(_patch_cmd "")
|
||||
if(WIN32)
|
||||
# Build-time Python for PCbuild's scripts, pinned because 3.15 cannot run the 3.12 scripts.
|
||||
set(DEP_PYTHON_HOST "" CACHE FILEPATH
|
||||
"Python 3.10-3.13 that runs CPython's build scripts; empty downloads a pinned one from nuget.org")
|
||||
if(DEP_PYTHON_HOST)
|
||||
execute_process(
|
||||
COMMAND "${DEP_PYTHON_HOST}" -E -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
||||
OUTPUT_VARIABLE _python_host_minor
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE _python_host_result
|
||||
)
|
||||
if(NOT _python_host_result EQUAL 0 OR _python_host_minor VERSION_LESS 3.10 OR _python_host_minor VERSION_GREATER 3.13)
|
||||
message(FATAL_ERROR "DEP_PYTHON_HOST must be Python 3.10 to 3.13, got '${_python_host_minor}' from ${DEP_PYTHON_HOST}")
|
||||
endif()
|
||||
file(TO_NATIVE_PATH "${DEP_PYTHON_HOST}" _python_host)
|
||||
else()
|
||||
set(_python_host_version "3.13.16")
|
||||
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")
|
||||
set(_python_host_package pythonarm64)
|
||||
set(_python_host_sha256 bfd77aa5b0edff4a7936a3220e8b79e7c78f864ce13e8fa2c835d9946e11694a)
|
||||
else()
|
||||
set(_python_host_package pythonx86)
|
||||
set(_python_host_sha256 c65cf47664b33286dfc5f2a17303668dd59e057869815ed9b270fb3866c1dd15)
|
||||
endif()
|
||||
set(_python_host_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_host-prefix/src/dep_python3_host")
|
||||
ExternalProject_Add(dep_python3_host
|
||||
URL "https://api.nuget.org/v3-flatcontainer/${_python_host_package}/${_python_host_version}/${_python_host_package}.${_python_host_version}.nupkg"
|
||||
URL_HASH SHA256=${_python_host_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
# Renames the .nupkg to .zip so ExternalProject extracts it.
|
||||
DOWNLOAD_NAME ${_python_host_package}.${_python_host_version}.zip
|
||||
SOURCE_DIR "${_python_host_dir}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
file(TO_NATIVE_PATH "${_python_host_dir}/tools/python.exe" _python_host)
|
||||
set(_python_depends dep_python3_host)
|
||||
endif()
|
||||
|
||||
# Fix python build failure on Windows if python is not available, due to wrong nuget download URL
|
||||
# See https://github.com/python/cpython/issues/153438
|
||||
# Patch from https://github.com/python/cpython/pull/153608
|
||||
# This patch has not been merged to 3.12 yet so we need to apply it manually
|
||||
#
|
||||
# Without core.autocrlf=false the patched find_python.bat comes out LF and
|
||||
# cmd.exe cannot find its goto labels.
|
||||
set(_patch_cmd git init
|
||||
&& ${GIT_EXECUTABLE} -c core.autocrlf=false apply --verbose
|
||||
--ignore-space-change --whitespace=fix
|
||||
${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
|
||||
if(MSVC_VERSION EQUAL 1800)
|
||||
set(_python_platform_toolset v120)
|
||||
@@ -88,47 +63,6 @@ if(WIN32)
|
||||
# release python3xx.lib; Py_DEBUG could not load release plugin modules.
|
||||
set(_python_pcbuild_config Release)
|
||||
|
||||
# CPython's own libraries, fetched here instead of by get_externals.bat, which ignores failed
|
||||
# downloads. Must match PCbuild/get_externals.bat for _python_version.
|
||||
set(_python_externals
|
||||
# repo folder commit sha256 of the archive
|
||||
source-deps bzip2-1.0.8 05301997b2f9590f49c672cf3dfd3d3dfa7ad521 44b15bede52b32f8673a2976621a847258b041243c003e30f4d9a05214785fe5
|
||||
source-deps sqlite-3.49.1.0 cb7a3997059739fe9c63b2819aeda7c6907510ef 586fe469463f5b27b749f9eef1938baa8e8a12030341e96160a371356c2d6759
|
||||
source-deps xz-5.2.5 c6bc0c612605622aaef101a33a751f9de2ecc193 d99882a8f9fd48be0e605e27e3c077e5eaab370f6f52ebcc8ff754480ffdf0bc
|
||||
source-deps zlib-1.3.1 4dc98e1909830e2bdc2a9cc2236e3c5d5037335b 39d867c083ebc37c77cf03a442f87c0f83db006eb64343bd4a19f1c84c26f50c
|
||||
bin-deps libffi-3.4.4 94cb9a1c7feb608adf2b9f8fe2dbd6925ffbf90d 6b0f53490e0dd958708bff32df689cf2163c214147fcb8edb76a403fa6a877ca
|
||||
bin-deps openssl-bin-3.0.16.2 1174fd43d858425c345b16d631716fb98ba492ea b741fd1411a36cb899fee2c39b70a4492f4bc1c6e2301d67df32e9754118a63b
|
||||
)
|
||||
set(_python_externals_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_externals")
|
||||
list(LENGTH _python_externals _python_externals_len)
|
||||
math(EXPR _python_externals_last "${_python_externals_len} - 1")
|
||||
foreach(_i RANGE 0 ${_python_externals_last} 4)
|
||||
math(EXPR _j "${_i} + 1")
|
||||
math(EXPR _k "${_i} + 2")
|
||||
math(EXPR _l "${_i} + 3")
|
||||
list(GET _python_externals ${_i} _ext_repo)
|
||||
list(GET _python_externals ${_j} _ext_name)
|
||||
list(GET _python_externals ${_k} _ext_commit)
|
||||
list(GET _python_externals ${_l} _ext_sha256)
|
||||
ExternalProject_Add(dep_python3_ext_${_ext_name}
|
||||
URL "https://github.com/python/cpython-${_ext_repo}/archive/${_ext_commit}.zip"
|
||||
URL_HASH SHA256=${_ext_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
DOWNLOAD_NAME cpython-${_ext_repo}-${_ext_name}.zip
|
||||
SOURCE_DIR "${_python_externals_dir}/${_ext_name}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
list(APPEND _python_depends dep_python3_ext_${_ext_name})
|
||||
endforeach()
|
||||
file(TO_NATIVE_PATH "${_python_externals_dir}" _python_externals_native)
|
||||
|
||||
# PCbuild runs PYTHON without -E, so PYTHONHOME and PYTHONPATH are cleared.
|
||||
set(_python_env_args --unset=PYTHONHOME --unset=PYTHONPATH "PYTHON=${_python_host}"
|
||||
"EXTERNALS_DIR=${_python_externals_native}"
|
||||
"GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
|
||||
# CPython's PCbuild needs a 64-bit-hosted toolchain: find_msbuild.bat picks the
|
||||
# 32-bit Bin\MSBuild.exe, whose x86 cl.exe/link.exe run out of address space
|
||||
# (fatal C1002 "out of heap space") building the LTCG-optimized pythoncore.
|
||||
@@ -137,6 +71,7 @@ if(WIN32)
|
||||
# then hosts arm64 tools on its own; the amd64 MSBuild still defaults to x86, so
|
||||
# PreferredToolArchitecture pins it to x64. Scoped to this build step, so
|
||||
# CPython's own sources stay untouched.
|
||||
set(_python_env_args "GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
if(CMAKE_GENERATOR_INSTANCE) # empty for non-VS generators (e.g. Ninja)
|
||||
set(_python_msbuild "${CMAKE_GENERATOR_INSTANCE}/MSBuild/Current/Bin/${_python_msbuild_host}/MSBuild.exe")
|
||||
if(EXISTS "${_python_msbuild}")
|
||||
@@ -349,12 +284,12 @@ endif()
|
||||
ExternalProject_Add(dep_python3
|
||||
URL "${_python_url}"
|
||||
URL_HASH SHA256=${_python_sha256}
|
||||
PATCH_COMMAND ${_patch_cmd}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
BUILD_IN_SOURCE ON
|
||||
CONFIGURE_COMMAND ${_conf_cmd}
|
||||
BUILD_COMMAND ${_build_cmd}
|
||||
INSTALL_COMMAND ${_install_cmd}
|
||||
DEPENDS ${_python_depends}
|
||||
)
|
||||
|
||||
# Python depends on OpenSSL and ZLIB
|
||||
|
||||
@@ -88,6 +88,18 @@ 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.
|
||||
*/
|
||||
@@ -1113,22 +1125,22 @@ private:
|
||||
|
||||
auto d = cb - ci;
|
||||
|
||||
// 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)));
|
||||
// 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;
|
||||
}
|
||||
|
||||
// BBS make sure the item won't clash with excluded regions
|
||||
// do we have wipe tower after arranging?
|
||||
@@ -1157,7 +1169,9 @@ private:
|
||||
return;
|
||||
}
|
||||
Item objs_convex_hull_item(objs_convex_hull);
|
||||
Vertex ref_aligned = objs_convex_hull_item.referenceVertex() + d;
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# Focus mode (hiding inactive plates) — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
"Focus mode" is an entry of the canvas menu in the bottom left corner of Prepare. Turned on (eye
|
||||
lit), the 3D view hides the inactive plates and shows the current plate alone, so a project with
|
||||
many plates can be worked on one plate at a time. Turned off, every plate is shown. It is a view
|
||||
setting: it changes nothing in the model, the slice or the project file, and it does not reach
|
||||
plate thumbnails. The choice is kept in the app config as `focus_mode`, off by default.
|
||||
|
||||
Preview needs no such option, as it already draws the current plate alone and has its own plate
|
||||
toolbar. The assembly view and the Design tab do not lay objects out on plates, so the entry is
|
||||
disabled there.
|
||||
|
||||
## What is shown
|
||||
|
||||
The current plate is drawn with its bed, label and icons; the other plates are not drawn at all. The
|
||||
world axes, which Prepare draws at the corner of the first plate, move to the corner of the current
|
||||
plate, where Preview draws them.
|
||||
|
||||
An object instance is shown when its footprint, the XY bounding box of the convex hulls of its
|
||||
volumes, overlaps the current plate, or when it is selected. The test is made per instance, so the
|
||||
parts of an object are shown or hidden together, and a wipe tower follows its plate the same way.
|
||||
It is geometric rather than the plate membership `PartPlateList` keeps, because an instance stays on
|
||||
the plate it was first placed on: an object of another plate that reaches over this one, or one too
|
||||
large for any plate, would otherwise be invisible while it takes room on the current plate. A
|
||||
selected instance stays visible so that what a selection or a gizmo acts on is never hidden, for
|
||||
instance an object picked in the object list on another plate.
|
||||
|
||||
## Hiding
|
||||
|
||||
A hidden object is neither drawn nor picked. `GLCanvas3D::_update_inactive_plates_visibility()`
|
||||
runs at the start of each frame of Prepare. It clears `GLVolume::is_active` on the volumes of the
|
||||
hidden instances and deactivates their raycasters, which every render pass, the hover and rectangle
|
||||
selection, the shadow and center of mass markers already respect. It also deactivates the raycasters
|
||||
of the other plates, whose icons would otherwise still take clicks where nothing is drawn. The state
|
||||
persists between frames, so the raycasts made while handling the mouse, such as the anchor of a pan,
|
||||
see the same scene as the frame.
|
||||
|
||||
Other features clear `is_active` too: gizmos hide the objects they do not edit and show them all
|
||||
again when they close. `GLVolume::on_hidden_plate` records which volumes this update hid, so it only
|
||||
shows again what it hid itself, and it waits for a gizmo that hides objects to close before doing
|
||||
so. A volume another feature made active again is hidden again on the next frame. While Focus mode
|
||||
is off and nothing is left hidden, the update returns at once.
|
||||
|
||||
## Switching plates
|
||||
|
||||
The other plates cannot be clicked while Focus mode hides them, so a compact counterpart of the Preview
|
||||
plate toolbar takes their place: a column of plate thumbnails in the top left corner, below the top
|
||||
toolbar, shown when the project has more than one plate and no gizmo is open. The thumbnails are the
|
||||
ones the Preview toolbar shows (`PartPlate::thumbnail_data`); the canvas rebuilds its textures when
|
||||
`Plater::plate_toolbar_image_version()` changes, a counter each consumer tracks on its own so that the
|
||||
Preview toolbar's dirty flag is left alone. That counter moves after almost every edit, so the
|
||||
thumbnail of the current plate, the only one edited while the others are hidden, is drawn again each
|
||||
time, and the other plates' only once an edit to them has invalidated theirs. The current plate has an
|
||||
accent border, a hovered one the gray border of the Preview toolbar, empty plates are dimmed and the
|
||||
plate number sits in the corner; the tooltip gives the plate name. A click selects the plate and
|
||||
clears the selection, as a click on a plate row of the object list does. The column scrolls when it
|
||||
does not fit above the 3D navigator or the canvas toolbar. Its scrollbar works as the Preview
|
||||
toolbar's, with which it shares its style: it shows only while the mouse is over the column or drags
|
||||
the bar, and the mouse wheel scrolls the column either way.
|
||||
|
||||
Whatever selects a plate while the others are hidden, the camera moves its target over the new plate
|
||||
and keeps its angle, distance and height (`PartPlateList::select_plate()`), so the plate comes into
|
||||
view where the previous one was. A zoom to all plates, as after loading a project or adding a plate,
|
||||
frames the current plate instead. Turning Focus mode on does the same, so the current plate fills the view.
|
||||
@@ -1,135 +0,0 @@
|
||||
# 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.
|
||||
@@ -15,9 +15,8 @@ Outlines of centerlines that cross each other overlap at every crossing, which
|
||||
over-extrudes the wall intersections. The line-crossing patterns Grid,
|
||||
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
|
||||
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
|
||||
Support Cubic (`FillAdaptive`). 3D Honeycomb skips `multiline_fill()` and draws
|
||||
its N lines itself as parallel copies of each wave, which never cross
|
||||
(`Fill3DHoneycomb`). The other patterns outline their usual centerlines.
|
||||
Support Cubic (`FillAdaptive`); the other patterns outline their usual
|
||||
centerlines.
|
||||
|
||||
## Non-crossing centerlines
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ 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.
|
||||
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)).
|
||||
|
||||
A strong modifier without a usable segment, even one whose fragments were all
|
||||
discarded, passes the turn to the next one.
|
||||
@@ -230,23 +232,20 @@ 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 a whole perimeter can be painted in either color:
|
||||
follows seam painting, where painting a whole perimeter green is a meaningful
|
||||
choice and forbidding the seam all round is not:
|
||||
|
||||
- **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** 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.
|
||||
- **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.
|
||||
|
||||
Enforced, Neutral and Blocked take part in the usual priority order (see
|
||||
Enforced and Neutral take part in the usual priority order (see
|
||||
[Weak modifiers](#weak-modifiers)).
|
||||
|
||||
The perimeter is fully contained when the united intervals cover every source
|
||||
@@ -312,9 +311,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 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
|
||||
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
|
||||
carry their positions on the source contour; these remain as provenance after
|
||||
insertion and are not indices into the modified polygon.
|
||||
|
||||
@@ -385,10 +384,9 @@ 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 "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)".
|
||||
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)".
|
||||
The causes are:
|
||||
|
||||
- **failed to process some intersections (types):** at least one fragment was
|
||||
@@ -397,18 +395,8 @@ 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 or Right modifier was skipped for a
|
||||
(types):** a Seam Center, Left, Right or Blocked 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
|
||||
@@ -432,8 +420,6 @@ 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
|
||||
@@ -577,14 +563,12 @@ 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" 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
|
||||
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
|
||||
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 perimeter blocked all round under every seam position, a modifier with a
|
||||
hole, and the user warning.
|
||||
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
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
# Tooltip illustrations
|
||||
|
||||
The texture-displacement panel's tooltips can carry a picture under their text: a still image, or a
|
||||
short animation that loops while the tooltip is open. Several settings in that panel describe a shape
|
||||
rather than a number - what Rotation turns, what Tile repeats, what Invert turns inside out - and a
|
||||
sentence about a shape is read twice and understood once.
|
||||
|
||||
## Where a tooltip is drawn
|
||||
|
||||
`GLGizmoTextureDisplacement::panel_tooltip()` replaces `ImGuiWrapper::tooltip()` throughout the panel.
|
||||
It differs in two ways.
|
||||
|
||||
It places the tooltip beside the panel instead of under the mouse. The panel is pinned to the right
|
||||
edge of the canvas, so a tooltip following the mouse covers the control it is explaining, and one
|
||||
carrying a picture covers most of the panel with it. The tooltip goes to the left of
|
||||
`m_tooltip_anchor_min`/`_max` - the panel's own screen rectangle, or the texture picker's while that
|
||||
popup is up and the tooltip comes from inside it - and to the right only when there is no room on the
|
||||
left. This is the placement the picker popup itself already uses.
|
||||
|
||||
ImGui normally keeps a tooltip glued to the mouse and ignores `SetNextWindowPos()`. Orca's imgui
|
||||
branch honours a position set for a tooltip (see the `ImGuiWindowFlags_Tooltip` branch of `Begin()` in
|
||||
`deps_src/imgui/imgui.cpp`) and still clamps the result inside the canvas, which is what keeps a tall
|
||||
tooltip near the bottom edge on screen.
|
||||
|
||||
It also gives the tooltip a fixed width rather than one derived from its text, because a picture has
|
||||
to be laid out against something, and a column of tooltips that are all one width is steadier to read.
|
||||
|
||||
## Where the pictures live
|
||||
|
||||
`resources/images/texture_displacement/`, as PNG, JPG, BMP or GIF. A tooltip names them by file:
|
||||
|
||||
```cpp
|
||||
hover_tip(_u8L("Repeats the texture across the painted area. ..."),
|
||||
{ { "tile_off.png", _L("Off") }, { "tile_on.png", _L("On") } });
|
||||
```
|
||||
|
||||
Several in one tooltip are laid out side by side at equal widths, which is how a setting is shown as a
|
||||
comparison - the same model at two values of it. The caption under each is translated; the file name
|
||||
is not.
|
||||
|
||||
A file that is missing or will not decode is skipped, and a tooltip whose pictures are all missing is
|
||||
exactly the text tooltip it was before. So the art for a tooltip can land after the tooltip does, and
|
||||
a half-finished set never breaks the panel.
|
||||
|
||||
## When a tooltip appears
|
||||
|
||||
A tooltip waits for the pointer to rest on its control for 0.45 s. The panel is dense and the tooltip
|
||||
lands on the model beside it, so with no wait a sweep from the top of the panel down to Bake flashes a
|
||||
dozen large panels over the scene. Once one has just been up, the wait drops to 0.1 s for 0.7 s
|
||||
afterwards, so reading several settings in a row stays quick - but not to zero, or that same sweep
|
||||
would flash again the moment one tooltip had been seen.
|
||||
|
||||
No tooltip appears at all while a control is being used - a slider dragged, a value typed into one, a
|
||||
combo open, a layer card being dragged. The pointer sits on the very thing being edited, so a tooltip
|
||||
there covers the model exactly while the user is watching it change. The full wait comes back
|
||||
afterwards rather than the short one: having just let go of a slider is not a reason to be shown a
|
||||
picture of it.
|
||||
|
||||
A pointer resting on a control produces no events, and the canvas only renders when something asks it
|
||||
to, so the wait has to ask for its own frames, and so does an animation once one is on screen. So does
|
||||
a tooltip that has just appeared, animated or not: ImGui hides a window on the first frame it is
|
||||
submitted while it measures it, and does so every time a tooltip reappears because those windows are
|
||||
recycled, so the frame a tooltip is decided on is never the frame it is seen on. Both use
|
||||
`GLCanvas3D::request_extra_frame()`, which ends `on_idle()` with `evt.RequestMore()` and so obliges wx
|
||||
to deliver the next idle. `schedule_extra_frame()` is not enough on its own: it only arranges a timer
|
||||
whose event an idle event is assumed to follow, and on_idle renders nothing unless the canvas was
|
||||
already dirty when it ran. Asking for frames as fast as they come is safe here because which frame is
|
||||
drawn comes from the clock, not from the frame count, so a redraw that arrives early simply draws the
|
||||
same frame again.
|
||||
|
||||
## Decoding and playback
|
||||
|
||||
`slic3r/GUI/AnimatedTexture.hpp` decodes a file once and uploads one GL texture per frame. Stills go
|
||||
through `wxImage`; GIFs go through `wxGIFDecoder`, whose raw palette indices are composited here frame
|
||||
by frame, replaying the disposal method each frame asks for. That replay is what makes an animation
|
||||
look continuous: a GIF frame is a sub-rectangle drawn over what came before it, so an animation that
|
||||
only re-sends the pixels that changed draws as a flicker of disconnected patches without it.
|
||||
|
||||
A frame delay of 0 or 1 hundredths of a second is read as 100 ms, the convention browsers settle on,
|
||||
so an animation authored against that plays at the speed it was authored at.
|
||||
|
||||
The animation restarts when its tooltip opens, rather than running off a free-running clock: these are
|
||||
short loops of one setting going from one value to another, and joining one halfway through shows the
|
||||
end of a change the user never saw the start of.
|
||||
|
||||
Which frame is drawn is measured on a real clock, not `ImGui::GetTime()`. Nothing in OrcaSlicer ever sets
|
||||
`io.DeltaTime`, so it keeps imgui's default of 1/60 per frame and imgui's clock counts frames rather
|
||||
than seconds. On a canvas that renders only when asked, that is not a clock at all: an animation driven
|
||||
by it crawls while the canvas is idle and races while a moving mouse is rendering it flat out.
|
||||
|
||||
## Making one
|
||||
|
||||
`scripts/gen_texture_displacement_tips.py` draws the lot, in four languages, picked by what the
|
||||
setting actually is:
|
||||
|
||||
- **A shaded relief**, for the settings that are about a pattern: Depth, Tile, Tile size, Rotation,
|
||||
Repeat/Mirror, Smoothing, Edge fade, Invert, Blend and the Normal, Fast, Checker and Distortion
|
||||
views. One motif - a ring inside a square frame - runs through all of them, because using one shape
|
||||
everywhere is what lets two pictures be compared: whatever differs between them is the setting,
|
||||
since nothing else is.
|
||||
- **A flat mesh of triangles**, for the settings that are about the mesh or about a tool: the paint
|
||||
tools, brush size, the connected-area fill, subdivision, feature-adaptive refinement, remeshing,
|
||||
hard edges, slivers and the wireframe view. These carry the one accent colour in the set, meaning
|
||||
"painted", because that is the only thing in such a picture that is not geometry.
|
||||
- **A section**, for Midlevel and for smoothing the geometry. The reason generalises: shading reads
|
||||
slopes, so a height field moved up or down as a whole shades identically to where it started, and
|
||||
any setting whose meaning is *where the surface sits relative to where it was* is invisible from
|
||||
above and has to be drawn from the side.
|
||||
- **A flat ramp of greys**, for Mix filaments, Mixed colors and Denoise. Those are about which
|
||||
filament a spot ends up as; lighting one would only add a second thing changing between two
|
||||
pictures meant to differ in one.
|
||||
|
||||
The five Mapping pictures are one ball with a checker laid on it five ways, which is the whole of what
|
||||
choosing a projection does - except Unwrap, which is not a projection onto the model at all and so is
|
||||
drawn as the flat layout it produces.
|
||||
|
||||
Captures of the real tool are still the first choice for anything involving a real model or real
|
||||
filament colour: Bake, the mixed-colour count on a photograph, Adjust placement, the frame window and
|
||||
the UV editor's seam tool have no picture yet for that reason.
|
||||
|
||||
Either way:
|
||||
|
||||
- Square, around 256 px (192 for an animation, where every frame ships). The tooltip is about 20 em
|
||||
wide, so that is roughly native size on a 100% display and downscales cleanly above it. A single
|
||||
picture is capped at 10 em tall and centred, two side by side take half the width each.
|
||||
- Neutral grey. These say what a shape does; colour in one reads as a third thing the setting does,
|
||||
and has to work against both themes.
|
||||
- Animations loop seamlessly and stay short - 16 frames at 100 ms, under about 150 KB. They sit in
|
||||
`resources/`, which ships with every copy of the application. A sweep driven by a cosine over the
|
||||
frame count loops with no step, since it starts and ends at the same value and the same rate.
|
||||
- No text in the picture. Captions are translated; pixels are not.
|
||||
@@ -72,8 +72,6 @@ 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
|
||||
@@ -82,7 +80,6 @@ src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoAssembly.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoAssembly.hpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp
|
||||
src/slic3r/GUI/GUI.cpp
|
||||
src/slic3r/GUI/GUI_App.cpp
|
||||
src/slic3r/GUI/GUI_AuxiliaryList.cpp
|
||||
@@ -113,7 +110,6 @@ 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
|
||||
@@ -142,7 +138,6 @@ 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
|
||||
@@ -152,6 +147,7 @@ src/slic3r/GUI/HMSPanel.cpp
|
||||
src/slic3r/GUI/MainFrame.cpp
|
||||
src/slic3r/GUI/MediaPlayCtrl.cpp
|
||||
src/slic3r/GUI/MediaFilePanel.cpp
|
||||
src/slic3r/GUI/OrcaFilesPanel.cpp
|
||||
src/slic3r/GUI/ImageGrid.cpp
|
||||
src/slic3r/GUI/Widgets/ImageSwitchButton.cpp
|
||||
src/slic3r/GUI/Printer/PrinterFileSystem.cpp
|
||||
@@ -170,7 +166,6 @@ 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
|
||||
@@ -215,18 +210,13 @@ src/libslic3r/GCode.cpp
|
||||
src/libslic3r/GCodeWriter.cpp
|
||||
src/libslic3r/GCode/ToolOrdering.cpp
|
||||
src/libslic3r/GCode/SeamPlacer.cpp
|
||||
src/libslic3r/GCode/GCodeStatistics.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
|
||||
@@ -264,7 +254,6 @@ 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
|
||||
@@ -319,8 +308,6 @@ 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
|
||||
@@ -361,27 +348,11 @@ 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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1="7.5" y1=".5" x2="7.5" y2="2.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="6.5" x2="7.5" y2="8.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="12.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="5.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="1.5" y="8.5" width="12" height="4" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 684 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1="7.5" y1=".5" x2="7.5" y2="2.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="6.5" x2="7.5" y2="8.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="12.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="5.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="1.5" y="8.5" width="12" height="4" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 684 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/><line x1="14.5" y1=".5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="8.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 439 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/><line x1="14.5" y1=".5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="8.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 439 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/><line x1=".5" y1=".5" x2=".5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 435 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/><line x1=".5" y1=".5" x2=".5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 435 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="7.5" x2="2.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="6.5" y1="7.5" x2="8.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="12.5" y1="7.5" x2="14.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="5.5" width="4" height="4" transform="translate(-3 12) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 765 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="7.5" x2="2.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="6.5" y1="7.5" x2="8.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="12.5" y1="7.5" x2="14.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="5.5" width="4" height="4" transform="translate(-3 12) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 765 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1=".5" x2="14.5" y2=".5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" transform="translate(0 9) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 514 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1=".5" x2="14.5" y2=".5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" transform="translate(0 9) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 514 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="14.5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="8.5" width="4" height="4" transform="translate(-6 15) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 520 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="14.5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="8.5" width="4" height="4" transform="translate(-6 15) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 520 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 7.5 7.5 10.5 .5 7.5 7.5 4.5 14.5 7.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/><polyline points="9.5 .5 7.5 2.5 5.5 .5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="5.5 14.5 7.5 12.5 9.5 14.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 479 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 7.5 7.5 10.5 .5 7.5 7.5 4.5 14.5 7.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/><polyline points="9.5 .5 7.5 2.5 5.5 .5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="5.5 14.5 7.5 12.5 9.5 14.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 479 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/><line x1="7.5" y1="8.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="5.5 10.5 7.5 8.5 9.5 10.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 478 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/><line x1="7.5" y1="8.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="5.5 10.5 7.5 8.5 9.5 10.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 478 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 11.5 7.5 14.5 .5 11.5 7.5 8.5 14.5 11.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/><line x1="7.5" y1=".5" x2="7.5" y2="6.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="9.5 4.5 7.5 6.5 5.5 4.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 479 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 11.5 7.5 14.5 .5 11.5 7.5 8.5 14.5 11.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/><line x1="7.5" y1=".5" x2="7.5" y2="6.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="9.5 4.5 7.5 6.5 5.5 4.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 479 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="-2.5" y="5.5" width="10" height="4" transform="translate(-5 10) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="7.5" y="5.5" width="10" height="4" transform="translate(5 20) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="7.5" y1="5.5" x2="7.5" y2="9.5" style="fill:none; stroke:#7c8282; stroke-linecap:square; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 522 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="-2.5" y="5.5" width="10" height="4" transform="translate(-5 10) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="7.5" y="5.5" width="10" height="4" transform="translate(5 20) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="7.5" y1="5.5" x2="7.5" y2="9.5" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 522 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y=".5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="2.5" y="10.5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="9.5" y1="7.5" x2="5.5" y2="7.5" style="fill:none; stroke:#7c8282; stroke-linecap:square; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 440 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y=".5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="2.5" y="10.5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="9.5" y1="7.5" x2="5.5" y2="7.5" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-miterlimit:10;"/></svg>
|
||||
|
Before Width: | Height: | Size: 440 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 10.5 .5 11.5 7.5 14.5 14.5 11.5 12.17 10.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 6.5 .5 7.5 7.5 10.5 14.5 7.5 12.17 6.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 476 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 10.5 .5 11.5 7.5 14.5 14.5 11.5 12.17 10.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 6.5 .5 7.5 7.5 10.5 14.5 7.5 12.17 6.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB |