Compare commits

..
Author SHA1 Message Date
Rodrigo Faselli c030131bcc Merge branch 'main' into perf/slicing-optimizations 2026-10-11 22:39:31 -03:00
Rodrigo Faselli ac8ac299a5 add missing headers
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-11 22:36:16 -03:00
Rodrigo Faselli 0f48982b78 Merge branch 'main' into perf/slicing-optimizations 2026-10-11 15:00:58 -03:00
SoftFever f6e8821b13 Merge branch 'main' into perf/slicing-optimizations 2026-10-10 01:09:46 +08:00
SoftFever 076d88f34e Merge branch 'main' into perf/slicing-optimizations 2026-10-09 22:19:27 +08:00
SoftFever 7f554fde04 Restore the internal bridge expansion from #16056 lost in the main merge 2026-10-09 22:11:47 +08:00
SoftFever d704ee5538 Merge branch 'main' into perf/slicing-optimizations
# Conflicts:
#	src/libslic3r/PerimeterGenerator.cpp
#	src/libslic3r/PrintObject.cpp
#	src/libslic3r/Support/TreeSupport.cpp
2026-10-09 19:48:26 +08:00
Rodrigo Faselli 7de1dc1548 Merge branch 'main' into perf/slicing-optimizations 2026-10-04 13:26:02 -03:00
Rodrigo Faselli b6140110f2 Merge branch 'main' into perf/slicing-optimizations 2026-10-03 10:57:31 -03:00
Ian Bassi 7b0e2f3ce5 Keep the G-code identical to main on Clipper2
Since main moved to Clipper2, parts of this branch no longer gave the same
G-code as main:

- bridge_over_infill dropped expand(limiting_area, 0.3 * flow.spacing()) as
  a no-op. The offset is below one unit, but Clipper2 still unites its
  result, which splits and merges touching polygons and so changes the
  anchor lines. Running it on the polygons next to the bridge gives main's
  anchors without the whole-layer pass.
- tsp_remove_crossings stopped at the first repeated ordering, where main
  runs on to its pn * pn cap. The loop is periodic from that point, so it
  now takes only the steps to the ordering main stops on.
- With a single tile, the tiled booleans now make the plain call instead of
  cutting the clip to the tile first.

The tiled boolean test compared rings exactly. With the safety offset a tile
unites only the clip polygons near it, and Clipper2 can then round a
crossing 1 unit differently, so that case allows 1 unit.

Comments that named ClipperLib now say Clipper, and
docs/HLSD/polygon-clipping.md describes the tiled booleans.

G-code of the five handy models in four configurations and of a baked
texture relief is byte-identical to main. Colour-painted models still
differ: segmenting each island on its own splits a colour's region into
different pieces than one diagram over the layer, which on one model also
changes the first layer's tool order.
2026-10-02 20:26:59 -03:00
ExPikaPaka d80c69341c Say what the code does, not what it replaced
The timings and the runs that never finished belong in the commit
messages, where they can be read against the change; a reader of the
code cannot check them. Kept the cost that still explains the design.

MultiPoint also spells out the consequence: a moved-from Polygon or
Polyline is now really empty where it used to silently keep its points.

The two wall spacing comments the parallel loop reindented are plain
ASCII now, so the whole file is.
2026-10-02 18:13:46 -03:00
ExPikaPaka 4a63a7d916 Compare the tiled booleans polygon by polygon
Area alone would pass on a result whose pieces were merged across tiles
or which kept the cut edges of the clip. The rings are compared after
rotating each to its lowest point and sorting, so only the ordering is
free. The fixture now also asserts it really is split into more than one
tile, which is the path being tested.
2026-10-02 18:13:46 -03:00
ExPikaPaka de1dfd0611 Hand the island's walls over instead of copying them
append(const ExtrusionEntity &) clones; the collection each island
produced was deep-copied into the layer's loops and then thrown away.
The no-overlap areas are moved as well.
2026-10-02 18:13:46 -03:00
ExPikaPaka 11a5971cef Include what the new code uses
The Windows build stopped on test_kdtree.cpp: it calls std::iota without
including <numeric>, which libstdc++ happens to pull in anyway. Added
there, and the same for <limits> and <algorithm>/<cmath> where the
recent changes rely on them being included by something else.
2026-10-02 18:13:45 -03:00
ExPikaPaka 82f37ddb3a Fix the Windows build: near and far are macros there
bridge_over_infill's helper for splitting polygons by proximity named
its locals near and far. The Windows headers define both as macros that
expand to nothing, so "Polygons near;" declared nothing and the uses of
it did not compile. Renamed; no behaviour change.
2026-10-02 18:13:37 -03:00
ExPikaPaka fb03d1a1cb Visit seam candidates as the search finds them
Collecting every candidate within the radius into a vector cost more
than the search itself. Same order, so the seams are unchanged;
align_seam_points ~19.6 s at 0.1 mm / 2000k, was ~21.
2026-10-02 18:13:37 -03:00
ExPikaPaka 2b4bdead73 Run a layer's regions in parallel where they are independent
detect_surfaces_type, process_external_surfaces and the vertical shells
each waited on their own heaviest layer in turn. The LOTR map plate
slices in ~10.5 min at 0.1 mm / 2000k, was ~11.5; ~87 s at normal
settings, was ~97.
2026-10-02 18:13:37 -03:00
ExPikaPaka b1f0d6c6f6 Move polygons instead of copying them on move
MultiPoint had no rvalue constructor, so the derived move constructors
bound to the const reference and copied; append reserved exactly, so
collecting pieces one by one was quadratic. Colour segmentation ~3 s at
0.1 mm / 2000k, was ~40, and ordinary prints gain too.
2026-10-02 18:13:37 -03:00
ExPikaPaka 14751a8b06 Project painted faces onto the shell layers per tile
Only the slices within the deepest shell offset decide the result, so
the work is done per tile of the face. Top and bottom segmentation
~130 s at 0.1 mm / 2000k, was ~180.
2026-10-02 18:13:36 -03:00
ExPikaPaka 9a86d79038 Tile the booleans on layers of many pieces
ClipperLib slows down with the number of edges on a scan line, and a
layer cut through a fine relief has tens of thousands of pieces.
detect_surfaces_type ~50 s at 0.1 mm / 2000k, was ~145.
2026-10-02 18:13:05 -03:00
ExPikaPaka d905f1a39b Merge colour and top/bottom regions per island
The merge took anything from 3 to 38 minutes at 0.1 mm / 2000k, now
~2.5. Every region is grouped with the islands it overlaps, so the
result is the same.
2026-10-02 18:12:41 -03:00
ExPikaPaka 84ec518f26 Slice fine texture relief without stalling
A colour texture baked at 0.1 mm / 2000k made the top layers thousands
of islands and slicing never finished. Colour segmentation runs per
island, the merge subtracts piece by piece, the support check tests only
nearby islands, and the travel ordering finds crossings through a grid.
2026-10-02 18:12:41 -03:00
ExPikaPaka 1fc153308f Generate walls and split solid infill in parallel
Same output, ~2.2 min for the LOTR map plate, was ~2.6.
2026-10-02 18:12:41 -03:00
ExPikaPaka 3167c3665a Run colour segmentation and vertical shells in parallel
Same output, ~2.6 min for the LOTR map plate, was ~2.9.
2026-10-02 18:12:40 -03:00
ExPikaPaka 6d34d83e78 Faster slicing of colour-painted layers
A layer split into ~1000 colour fragments (a colour texture baked over a
large top face) made several per-fragment loops redo whole-layer ClipperLib
work, so slicing took ~33 min; it now takes ~3 min with the same output.

- make_fills: clip the layer's no-overlap area to each expolygon's box
  before intersecting
- discover_vertical_shells: small-piece filter compares only against the
  nearby part of the layer
- bridge_over_infill: whole-layer union/diff/intersections restricted to the
  candidate's neighbourhood; fill boundary expanded once per spacing; anchor
  tree built only from lines crossing the scan range; bbox pre-check in the
  collision test; limiting outline taken directly instead of through
  expand(..., 0.3 * flow.spacing()), which offsets by 0.135 scaled units
  (flow.spacing() is in mm) and only cost a whole-layer pass per candidate
2026-10-02 18:11:17 -03:00
731 changed files with 24936 additions and 139836 deletions
-220
View File
@@ -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 - 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)` sub, it builds a `wxMouseEvent(wxEVT_MOTION)` from `wxGetMousePosition()`, calls `mainDropDown->mouseMove(ev)`
**directly** (nothing is posted), and calls `e.RequestMore()` to keep polling. **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 - **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", `DoSetSize` only uses `gtk_widget_set_size_request` ("gtk_window_resize does not work for GTK_WINDOW_POPUP",
`src/gtk/popupwin.cpp:181-188`). `src/gtk/popupwin.cpp:181-188`).
@@ -612,32 +607,6 @@ Details [source] (`Widgets/PopupWindow.cpp`).
``` ```
Cite: c0ae2bda99, b9ff15054f (`DropDown::mouseMove`, `DropDown::messureSize`, `DropDown::ShouldDismissOnTopWindowDeactivate`, Cite: c0ae2bda99, b9ff15054f (`DropDown::mouseMove`, `DropDown::messureSize`, `DropDown::ShouldDismissOnTopWindowDeactivate`,
`PopupWindow::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 - **Rule:** When shrinking a dropdown to fit above the display bottom, give up if fewer than two rows would
remain. remain.
**Why:** On Wayland the popup can land where almost no vertical space is left. Clamping to that sliver made an **Why:** On Wayland the popup can land where almost no vertical space is left. Clamping to that sliver made an
-29
View File
@@ -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"
-104
View File
@@ -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
+5 -1
View File
@@ -441,10 +441,14 @@ jobs:
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak' path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
- name: Deploy Flatpak to nightly release - name: Deploy Flatpak to nightly release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
uses: ./.github/actions/deploy-nightly uses: WebFreak001/deploy-nightly@v3.2.0
with: 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_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ 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 # The asset is /app (the exes link it at runtime) plus the build tree
# slimmed to what ctest needs. # slimmed to what ctest needs.
- name: Package flatpak test asset - name: Package flatpak test asset
+65 -48
View File
@@ -33,8 +33,6 @@ jobs:
ubuntu-ver: '2404' ubuntu-ver: '2404'
ubuntu-ver-str: '_Ubuntu2404' ubuntu-ver-str: '_Ubuntu2404'
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }} ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
NSIS_VERSION: '3.13'
NSIS_SHA256: BA63DFFC4410EE89193E1CB5A41989991BD77C61068DA17E3156D136B7B0B3D8
steps: steps:
- name: Checkout - name: Checkout
@@ -343,12 +341,12 @@ jobs:
# Notarize main DMG # Notarize main DMG
xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait xcrun 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 # Notarize profile validator DMG if it exists
if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then
xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait xcrun 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 fi
- name: Create DMG without notary - name: Create DMG without notary
@@ -399,18 +397,25 @@ jobs:
- name: Deploy Mac release - name: Deploy Mac release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED 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: 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_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_Mac_universal_nightly.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 - 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)) != '' 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: 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_path: ${{ github.workspace }}/OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_profile_validator_Mac_universal_nightly.dmg asset_name: OrcaSlicer_profile_validator_Mac_universal_nightly.dmg
asset_content_type: application/octet-stream
max_releases: 1
# Windows # Windows
- name: Set Windows build variables - name: Set Windows build variables
@@ -429,49 +434,36 @@ jobs:
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: microsoft/setup-msbuild@v3 uses: microsoft/setup-msbuild@v3
# NSIS builds the installer. A pinned portable copy is kept in the Actions - name: Install nsis
# 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
if: runner.os == 'Windows' && !vars.SELF_HOSTED if: runner.os == 'Windows' && !vars.SELF_HOSTED
shell: pwsh 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: | run: |
dir "C:/Program Files (x86)/Windows Kits/10/Include" 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' $makensis = Join-Path $nsisDir 'makensis.exe'
if (-not (Test-Path $makensis)) { for ($attempt = 1; $attempt -le 3 -and -not (Test-Path $makensis); $attempt++) {
$zip = Join-Path $env:RUNNER_TEMP "nsis-$env:NSIS_VERSION.zip" if ($attempt -gt 1) { Start-Sleep -Seconds (15 * $attempt) }
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" Write-Host "::group::choco install nsis (attempt $attempt)"
if ($LASTEXITCODE -ne 0) { throw "NSIS $env:NSIS_VERSION download failed." } choco install nsis --yes --no-progress
if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $env:NSIS_SHA256) { Write-Host "::endgroup::"
throw "NSIS $env:NSIS_VERSION archive does not match its pinned SHA256."
} }
Expand-Archive $zip $env:RUNNER_TEMP if (-not (Test-Path $makensis)) {
Move-Item (Join-Path $env:RUNNER_TEMP "nsis-$env:NSIS_VERSION") $nsisDir throw "NSIS install failed: $makensis not found after 3 attempts."
} }
& $makensis /VERSION & $makensis /VERSION
$nsisDir | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 $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 - name: Build slicer Win
if: runner.os == 'Windows' if: runner.os == 'Windows'
working-directory: ${{ github.workspace }} 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. # --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
run: | run: |
# cache-path is the install directory inside the deps build directory. # cache-path is the install directory inside the deps build directory.
@@ -554,27 +546,36 @@ jobs:
- name: Deploy Windows release portable - name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED 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: 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_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip asset_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 - name: Deploy Windows release installer
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED 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: 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_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe asset_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 - 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 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: 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_path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
asset_name: OrcaSlicer_profile_validator_Windows_nightly.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 - name: Build MSIX Store package Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED if: runner.os == 'Windows' && !vars.SELF_HOSTED
@@ -703,10 +704,14 @@ jobs:
- name: Deploy Ubuntu release - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }} 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: 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_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_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 - 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' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
run: | run: |
@@ -715,24 +720,36 @@ jobs:
- name: Deploy Ubuntu OrcaSlicer_profile_validator release - 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' }} 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: 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_path: ./build/src/Release/OrcaSlicer_profile_validator
asset_name: OrcaSlicer_profile_validator_Linux${{ env.ubuntu-ver-str }}_nightly 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 - 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' }} 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: 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_path: ./build/src/dev-utils/Release/generate_system_cache
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly 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 - 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' }} 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: 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_path: ${{ github.workspace }}/resources/profiles/orca_custom_preset_tests.zip
asset_name: 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 # The build has just touched everything it can use, so an object
# untouched for a week is dead, usually orphaned by a flag change. # untouched for a week is dead, usually orphaned by a flag change.
+2 -29
View File
@@ -13,7 +13,6 @@ on:
# both it and its tests live in scripts/, so a PR touching only those must still run # both it and its tests live in scripts/, so a PR touching only those must still run
# this workflow. # this workflow.
- 'resources/printers/**' - 'resources/printers/**'
- 'resources/info/legacy_compatible_printers.json'
- 'scripts/**' - 'scripts/**'
# orca_profile_tool.py reads the variant key sets from PrintConfig.cpp, and its # 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. # 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 python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log
exit ${PIPESTATUS[0]} 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 # download
- name: Download - name: Download
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
@@ -233,7 +217,7 @@ jobs:
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- name: Prepare comment artifact - name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: | run: |
{ {
# Marker matched by check_profiles_comment.yml to delete prior comments. # Marker matched by check_profiles_comment.yml to delete prior comments.
@@ -250,17 +234,6 @@ jobs:
echo "" echo ""
fi 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 if [ "${{ steps.validate_system.outcome }}" = "failure" ]; then
echo "### System Profile Validation Failed" echo "### System Profile Validation Failed"
echo "" echo ""
@@ -310,7 +283,7 @@ jobs:
retention-days: 1 retention-days: 1
- name: Fail if any check failed - 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: | run: |
echo "One or more profile checks failed; see the step logs above." 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).' echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).'
+4 -70
View File
@@ -16,15 +16,6 @@ on:
permissions: permissions:
contents: read 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: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 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. # The PR merge commit plus its first parent, the base it is diffed against.
fetch-depth: 2 fetch-depth: 2
- name: Look for changed files - name: Look for changed C++ files
id: changes id: changes
run: | run: |
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then 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 else
echo "No C++ changes under src/ or tests/." echo "No C++ changes under src/ or tests/."
fi 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/. # Parsing needs the dependency headers, not a build of this PR's deps/, so
# Without an exact match the job waits for the build that saves it: this # a PR that changes deps/ falls back to the newest cache main has.
# PR's own when it changes deps/, otherwise a running push build of the
# base branch with the same deps. With neither, a PR that keeps deps/
# falls back to the newest cache.
- name: Restore cached deps - name: Restore cached deps
id: deps_cache
if: steps.changes.outputs.cpp == 'true' if: steps.changes.outputs.cpp == 'true'
uses: actions/cache/restore@v6 uses: actions/cache/restore@v6
with: with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }} key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
- name: Look for a build to wait for
id: wait_check
if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }}
# Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The
# wait ends early when the deps job fails.
- name: Wait for the deps cache
if: steps.wait_check.outputs.wait-for != ''
timeout-minutes: 100
env:
GH_TOKEN: ${{ github.token }}
KEY: ${{ steps.changes.outputs.deps-key }}
CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }}
KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }}
WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }}
WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }}
WAIT_MINUTES: 90
run: bash scripts/ci_deps_cache.sh wait
- name: Restore the deps cache after waiting
id: waited_cache
if: steps.wait_check.outputs.wait-for != ''
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
- name: Fail without this PR's deps cache
if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
run: |
echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy."
exit 1
- name: Restore the newest cached deps
id: newest_cache
if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
restore-keys: linux-clang-cache-orcaslicer_deps-build- restore-keys: linux-clang-cache-orcaslicer_deps-build-
fail-on-cache-miss: true 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 - name: Apt-Install Dependencies
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps uses: ./.github/actions/apt-install-deps
-14
View File
@@ -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. - 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. - 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 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. - 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 ## 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`. 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. 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 ### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated. - Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
+42 -2
View File
@@ -497,6 +497,47 @@ endif()
# Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC # Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC
set(CMAKE_POSITION_INDEPENDENT_CODE ON) 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) if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}") message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")
if (CMAKE_OSX_DEPLOYMENT_TARGET) 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_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/images\\\\OrcaSlicer.ico")
set (CPACK_NSIS_MUI_ICON "${CPACK_PACKAGE_ICON}") set (CPACK_NSIS_MUI_ICON "${CPACK_PACKAGE_ICON}")
set (CPACK_NSIS_MUI_UNIICON "${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 "$INSTDIR\\\\orca-slicer.exe")
set (CPACK_NSIS_INSTALLED_ICON_NAME "orca-slicer.exe")
set (CPACK_PACKAGE_CHECKSUM SHA256) set (CPACK_PACKAGE_CHECKSUM SHA256)
set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "OrcaSlicer") set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "OrcaSlicer")
set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
-11
View File
@@ -87,7 +87,6 @@ The [wiki](https://www.orcaslicer.com/wiki) aims to provide a detailed explanati
📥 **[Download the Latest Stable Release](https://github.com/OrcaSlicer/OrcaSlicer/releases/latest)** 📥 **[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. 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).
## Nightly Builds ## 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). 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 # License
- **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3. - **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3.
+3 -22
View File
@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null pushd "${SCRIPT_PATH}" > /dev/null
function usage() { 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 " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (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)" 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 " -u: install system dependencies (asks for sudo password; build prerequisite)"
echo " -l: use Clang instead of GCC (default: GCC)" echo " -l: use Clang instead of GCC (default: GCC)"
echo " -L: use ld.lld as linker (if available)" 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 "For a first use, you want to './${SCRIPT_NAME} -u'"
echo " and then './${SCRIPT_NAME} -dsi'" echo " and then './${SCRIPT_NAME} -dsi'"
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'" 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 "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 "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" SLIC3R_PRECOMPILED_HEADERS="ON"
@@ -135,20 +131,6 @@ while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
esac esac
done 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 if [ ${OPTIND} -eq 1 ] ; then
usage usage
exit 1 exit 1
@@ -565,7 +547,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}") BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
fi 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 # 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. # 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 # -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} \ -DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
-DORCA_TOOLS=ON \ -DORCA_TOOLS=ON \
"${COLORED_OUTPUT}" \ "${COLORED_OUTPUT}" \
"${BUILD_ARGS[@]}" \ "${BUILD_ARGS[@]}"
"${EXTRA_CMAKE_ARGS[@]}"
echo "done" echo "done"
if [[ -n "${BUILD_ORCA}" ]]; then if [[ -n "${BUILD_ORCA}" ]]; then
echo "Building OrcaSlicer ..." echo "Building OrcaSlicer ..."
-46
View File
@@ -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_slicer bool s slicer "Build OrcaSlicer"
call :add_arg build_tests bool "" tests "Build the unit tests" 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 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 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_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" 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" 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 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 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. 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%". echo --install needs the executable, but --slicer-target names "%slicer_target%".
exit /b 1 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%!" == "" ( if not "%vs_version%" == "" if "!vs_gen_%vs_version%!" == "" (
echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026. echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026.
exit /b 1 exit /b 1
@@ -762,14 +734,6 @@ if "%build_slicer%" == "ON" (
call :print_and_run cmake --build "%build_dir%" --target install --config %build_type% call :print_and_run cmake --build "%build_dir%" --target install --config %build_type%
%error_check% %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 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 "%linked%" == "ON" echo Target %slicer_target%
if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution! 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 "%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.
echo Next echo Next
-8
View File
@@ -1,18 +1,10 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style # 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 # 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. # 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) foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes") set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc) set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif () endif ()
endforeach () endforeach ()
@@ -88,6 +88,18 @@ struct NfpPConfig {
*/ */
bool explore_holes = false; 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. * @brief If true, use all CPUs available. Run on a single core otherwise.
*/ */
@@ -1113,11 +1125,9 @@ private:
auto d = cb - ci; auto d = cb - ci;
// Keep the pile on the bin. Centring it on a target near an edge (a belt // Keep the pile on the bin (see Config::clamp_to_bin). The items' boxes carry
// printer starts its parts at the leading end of the belt) would push a pile // their inflation, which is the margin left at the edge.
// larger than the room around that point off the bed, so it stops at the if (config_.clamp_to_bin) {
// 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) { auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
if (hi - lo >= bin_hi - bin_lo) if (hi - lo >= bin_hi - bin_lo)
return (bin_lo + bin_hi) / 2 - (lo + hi) / 2; return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
@@ -1129,6 +1139,8 @@ private:
}; };
setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d))); 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))); 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 // BBS make sure the item won't clash with excluded regions
// do we have wipe tower after arranging? // do we have wipe tower after arranging?
@@ -1157,7 +1169,9 @@ private:
return; return;
} }
Item objs_convex_hull_item(objs_convex_hull); 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); }); 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) { if (!ref_aligned_is_ok) {
// ref_aligned is not good, then find a nearest point on nfp boundary // ref_aligned is not good, then find a nearest point on nfp boundary
-66
View File
@@ -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.
-135
View File
@@ -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.
+2 -3
View File
@@ -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, over-extrudes the wall intersections. The line-crossing patterns Grid,
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and (`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
Support Cubic (`FillAdaptive`). 3D Honeycomb skips `multiline_fill()` and draws Support Cubic (`FillAdaptive`); the other patterns outline their usual
its N lines itself as parallel copies of each wave, which never cross centerlines.
(`Fill3DHoneycomb`). The other patterns outline their usual centerlines.
## Non-crossing centerlines ## Non-crossing centerlines
+17
View File
@@ -62,6 +62,23 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
clip do not leave slivers. clip do not leave slivers.
- Open polylines are clipped with the non-zero rule and keep their direction. - Open polylines are clipped with the non-zero rule and keep their direction.
### Tiled booleans
The sweep slows down with the number of edges crossing a scan line, so a layer
cut into thousands of pieces makes every whole-layer boolean expensive.
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
non-overlapping `ExPolygons`, group them into tiles with
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
The result covers the same area as the plain call. Without the safety offset
the rings are the same. With it, each tile unites only the clip polygons near
it, so a clip edge that the whole-layer union splits where it crosses a distant
clip polygon stays whole, and a crossing with the subject can round 1 unit
differently. The tiles' results are concatenated in tile order, so the order of
the output `ExPolygons` differs from the plain call.
### Offsets ### Offsets
- Before offsetting, input vertices closer than - Before offsetting, input vertices closer than
+22 -38
View File
@@ -82,7 +82,9 @@ there.
strong point is placed, no later strong modifier and no weak modifier is strong point is placed, no later strong modifier and no weak modifier is
processed for that perimeter. processed for that perimeter.
- **Weak:** every weak modifier applies. They are applied from the lowest - **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 A strong modifier without a usable segment, even one whose fragments were all
discarded, passes the turn to the next one. discarded, passes the turn to the next one.
@@ -230,23 +232,20 @@ begin and end positions on the source contour.
### Full containment ### Full containment
A modifier that covers the whole perimeter has no boundaries on it. The policy 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 - **Seam Enforced** types the whole perimeter, like a perimeter painted green all
round, with subdivision applied as described under [Weak modifiers](#weak-modifiers). round, with subdivision applied as described under [Weak modifiers](#weak-modifiers).
- **Seam Neutral** types the whole perimeter Neutral, like an unmarked perimeter, - **Seam Neutral** types the whole perimeter Neutral, like an unmarked perimeter,
clearing painting and lower zones. clearing painting and lower zones.
- **Seam Blocked** types the whole perimeter Blocked, like a perimeter painted - **Seam Blocked** is skipped for the perimeter, with the full-containment
red all round, overriding painting and lower zones. The seam cannot avoid such warning. The seam cannot avoid the whole perimeter, so the modifier does not
a perimeter: the seam placer compares Blocked candidates with each other like override anything below it: lower zones and painting stay in effect.
Neutral ones, so the seam is placed as on an unmarked perimeter. If the - **Seam Center, Left and Right** are skipped with the same warning: there is no
perimeter stays Blocked after all weak zones, it is reported in the user intersection to place the point on.
warning (see [Diagnostics and warnings](#diagnostics-and-warnings)), so the
user can check the modifier.
- **Seam Center, Left and Right** are skipped with the full-containment warning:
there is no intersection to place the point on.
Enforced, 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)). [Weak modifiers](#weak-modifiers)).
The perimeter is fully contained when the united intervals cover every source 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 `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 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 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 order, lowest priority first. Full containment of an Enforced or Neutral
whole-perimeter zone at its place in that order: it has no boundaries and takes modifier becomes a whole-perimeter zone at its place in that order: it has no
part in no insertion or helper step below. The boundaries 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 carry their positions on the source contour; these remain as provenance after
insertion and are not indices into the modified polygon. 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 `SlicingPreciseSeamWarning`. It is a single line, "Precise Seam: <causes>. Seam
placement may differ from expected.", because the export warnings dialog shows placement may differ from expected.", because the export warnings dialog shows
only the first line of each warning. Repeated warning events replace the only the first line of each warning. Repeated warning events replace the
notification instead of appending to it. Except for the "covers an entire notification instead of appending to it. Except for the "had no effect" cause,
perimeter" and "had no effect" causes, the causes name the modifier types the causes name the modifier types involved, as the menu names them, in menu
involved, as the menu names them, in menu order and each type once, for example order and each type once, for example "(Seam Left, Seam Enforced)".
"(Seam Left, Seam Enforced)".
The causes are: The causes are:
- **failed to process some intersections (types):** at least one fragment was - **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 a Seam Center, Left or Right modifier had more than one segment on a
perimeter (see [Strong modifiers](#strong-modifiers)). perimeter (see [Strong modifiers](#strong-modifiers)).
- **a perimeter is fully inside a modifier, the modifier was not applied to it - **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)). 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 - **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 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 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 - `[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 the object and modifier names. Unlike the user warning, the log lists all of
them. 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 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 `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 priorities, weak boundaries that coincide or share an edge, enforced
subdivision, whole-perimeter weak zones with painting and priorities, weak subdivision, whole-perimeter weak zones with painting and priorities, weak
zones over painting's oversampled candidates, the warning type masks, usage zones over painting's oversampled candidates, the warning type masks, usage
tracking for the "had no effect" and whole-perimeter Blocked warnings, volume tracking for the "had no effect" warning, volume sorting of strong and weak
sorting of strong and weak groups, restoration of strong points after groups, restoration of strong points after alignment, raft layer indexing and
alignment, raft layer indexing and structured slices. End-to-end tests slice structured slices. End-to-end tests slice a real object with Precise Seam
a real object with Precise Seam
volumes and check the outer wall starts in the exported G-code: every strong 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, 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 a modifier with a hole, and the user warning.
hole, and the user warning.
- [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover - [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover
enforced-patch selection independent of the contour start, fully painted enforced-patch selection independent of the contour start, fully painted
contours, duplicate removal, and `Print::apply()` synchronization through type contours, duplicate removal, and `Print::apply()` synchronization through type
-131
View File
@@ -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.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-30
View File
@@ -72,8 +72,6 @@ src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.cpp src/slic3r/GUI/Gizmos/GLGizmoCut.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.hpp src/slic3r/GUI/Gizmos/GLGizmoCut.hpp
src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp 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.cpp
src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp
src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp 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/GLGizmoMeasure.hpp
src/slic3r/GUI/Gizmos/GLGizmoAssembly.cpp src/slic3r/GUI/Gizmos/GLGizmoAssembly.cpp
src/slic3r/GUI/Gizmos/GLGizmoAssembly.hpp src/slic3r/GUI/Gizmos/GLGizmoAssembly.hpp
src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp
src/slic3r/GUI/GUI.cpp src/slic3r/GUI/GUI.cpp
src/slic3r/GUI/GUI_App.cpp src/slic3r/GUI/GUI_App.cpp
src/slic3r/GUI/GUI_AuxiliaryList.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/OrientJob.cpp
src/slic3r/GUI/Jobs/FillBedJob.cpp src/slic3r/GUI/Jobs/FillBedJob.cpp
src/slic3r/GUI/Jobs/RotoptimizeJob.cpp src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
src/slic3r/GUI/Jobs/RotoptimizeJob.hpp
src/slic3r/GUI/Jobs/BindJob.cpp src/slic3r/GUI/Jobs/BindJob.cpp
src/slic3r/GUI/Jobs/PrintJob.cpp src/slic3r/GUI/Jobs/PrintJob.cpp
src/slic3r/GUI/Jobs/SendJob.cpp src/slic3r/GUI/Jobs/SendJob.cpp
@@ -142,7 +138,6 @@ src/slic3r/GUI/DeviceErrorDialog.cpp
src/slic3r/GUI/ExtraRenderers.cpp src/slic3r/GUI/ExtraRenderers.cpp
src/slic3r/GUI/EditGCodeDialog.cpp src/slic3r/GUI/EditGCodeDialog.cpp
src/slic3r/GUI/Field.cpp src/slic3r/GUI/Field.cpp
src/slic3r/GUI/Field.hpp
src/slic3r/GUI/GCodeViewer.cpp src/slic3r/GUI/GCodeViewer.cpp
src/slic3r/GUI/GLCanvas3D.cpp src/slic3r/GUI/GLCanvas3D.cpp
src/slic3r/GUI/Calibration.cpp src/slic3r/GUI/Calibration.cpp
@@ -170,7 +165,6 @@ src/slic3r/GUI/PrintOptionsDialog.cpp
src/slic3r/GUI/SafetyOptionsDialog.cpp src/slic3r/GUI/SafetyOptionsDialog.cpp
src/slic3r/GUI/ParamsPanel.cpp src/slic3r/GUI/ParamsPanel.cpp
src/slic3r/GUI/IMEXModesCtrl.cpp src/slic3r/GUI/IMEXModesCtrl.cpp
src/slic3r/GUI/IMEXFilamentPickerPopover.cpp
src/slic3r/GUI/PartPlate.cpp src/slic3r/GUI/PartPlate.cpp
src/slic3r/GUI/Plater.cpp src/slic3r/GUI/Plater.cpp
src/slic3r/GUI/Preferences.cpp src/slic3r/GUI/Preferences.cpp
@@ -215,18 +209,13 @@ src/libslic3r/GCode.cpp
src/libslic3r/GCodeWriter.cpp src/libslic3r/GCodeWriter.cpp
src/libslic3r/GCode/ToolOrdering.cpp src/libslic3r/GCode/ToolOrdering.cpp
src/libslic3r/GCode/SeamPlacer.cpp src/libslic3r/GCode/SeamPlacer.cpp
src/libslic3r/GCode/GCodeStatistics.cpp
src/libslic3r/ExtrusionEntity.cpp src/libslic3r/ExtrusionEntity.cpp
src/libslic3r/Flow.cpp src/libslic3r/Flow.cpp
src/libslic3r/Format/AMF.cpp src/libslic3r/Format/AMF.cpp
src/libslic3r/Format/3mf.cpp
src/libslic3r/Format/bbs_3mf.cpp
src/libslic3r/miniz_extension.cpp src/libslic3r/miniz_extension.cpp
src/libslic3r/IMEXHelpers.cpp src/libslic3r/IMEXHelpers.cpp
src/libslic3r/Preset.cpp src/libslic3r/Preset.cpp
src/libslic3r/Print.cpp src/libslic3r/Print.cpp
src/libslic3r/BeltPurge.cpp
src/libslic3r/AppConfig.cpp
src/libslic3r/PrintBase.cpp src/libslic3r/PrintBase.cpp
src/libslic3r/PrintConfig.cpp src/libslic3r/PrintConfig.cpp
src/libslic3r/Zipper.cpp src/libslic3r/Zipper.cpp
@@ -264,7 +253,6 @@ src/slic3r/Utils/MKS.cpp
src/slic3r/Utils/Moonraker.cpp src/slic3r/Utils/Moonraker.cpp
src/slic3r/Utils/OctoPrint.cpp src/slic3r/Utils/OctoPrint.cpp
src/slic3r/Utils/Repetier.cpp src/slic3r/Utils/Repetier.cpp
src/slic3r/Utils/ESP3D.cpp
src/slic3r/GUI/SendMultiMachinePage.cpp src/slic3r/GUI/SendMultiMachinePage.cpp
src/slic3r/GUI/MultiMachinePage.cpp src/slic3r/GUI/MultiMachinePage.cpp
src/slic3r/GUI/MultiMachineManagerPage.cpp src/slic3r/GUI/MultiMachineManagerPage.cpp
@@ -319,8 +307,6 @@ src/slic3r/GUI/AuxiliaryDataViewModel.cpp
src/slic3r/GUI/AuxiliaryDialog.cpp src/slic3r/GUI/AuxiliaryDialog.cpp
src/slic3r/GUI/BBLStatusBarPrint.cpp src/slic3r/GUI/BBLStatusBarPrint.cpp
src/slic3r/GUI/BBLStatusBarSend.cpp src/slic3r/GUI/BBLStatusBarSend.cpp
src/slic3r/GUI/BBLStatusBar.cpp
src/slic3r/GUI/BeltPurgeTower.cpp
src/slic3r/GUI/BaseTransparentDPIFrame.cpp src/slic3r/GUI/BaseTransparentDPIFrame.cpp
src/slic3r/GUI/CloneDialog.cpp src/slic3r/GUI/CloneDialog.cpp
src/slic3r/GUI/ColorDecomposeDialog.cpp src/slic3r/GUI/ColorDecomposeDialog.cpp
@@ -361,27 +347,11 @@ src/slic3r/GUI/WebViewDialog.cpp
src/slic3r/GUI/Widgets/AMSItem.cpp src/slic3r/GUI/Widgets/AMSItem.cpp
src/slic3r/GUI/Widgets/DialogButtons.cpp src/slic3r/GUI/Widgets/DialogButtons.cpp
src/slic3r/Utils/NetworkAgentFactory.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/PluginAuditManager.cpp
src/slic3r/plugin/PluginConfig.cpp src/slic3r/plugin/PluginConfig.cpp
src/slic3r/plugin/PluginResolver.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/SketchEngine.cpp
src/libslic3r/CAD/GeometryEngine.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/DesignOffer.hpp
src/slic3r/GUI/CAD/DesignSketchTool.cpp src/slic3r/GUI/CAD/DesignSketchTool.cpp
src/slic3r/GUI/EncodedFilament.cpp src/slic3r/GUI/EncodedFilament.cpp
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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

-3
View File
@@ -1,3 +0,0 @@
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Some files were not shown because too many files have changed in this diff Show More