Compare commits

..
1120 changed files with 96474 additions and 197765 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
sub, it builds a `wxMouseEvent(wxEVT_MOTION)` from `wxGetMousePosition()`, calls `mainDropDown->mouseMove(ev)`
**directly** (nothing is posted), and calls `e.RequestMore()` to keep polling.
- On GTK the sub also receives the wheel events the user raises over main: GDK sends a `wl_pointer.axis` only
to the surface the pointer is over, and GTK then hands that `GDK_SCROLL` to the chain's grab widget, which is
the sub. The sub installs a `GtkWidget::event` handler that replays the event on main with
`gtk_propagate_event(main->GetConnectWidget(), event)` when the event's GDK window belongs to main
(`DropDown::messureSize`).
- **GTK wrapper sizing.** `messureSize` calls `gtk_window_resize` on `m_widget` after `SetSize`, because wxGTK's
`DoSetSize` only uses `gtk_widget_set_size_request` ("gtk_window_resize does not work for GTK_WINDOW_POPUP",
`src/gtk/popupwin.cpp:181-188`).
@@ -612,32 +607,6 @@ Details [source] (`Widgets/PopupWindow.cpp`).
```
Cite: c0ae2bda99, b9ff15054f (`DropDown::mouseMove`, `DropDown::messureSize`, `DropDown::ShouldDismissOnTopWindowDeactivate`,
`PopupWindow::ShouldDismissOnTopWindowDeactivate`).
- **Rule:** On GTK, route a chained popup's wheel event by the GDK window it names, not by the grab widget and not
by the global cursor position.
**Why:** While the sub holds the grab, `gtk_main_do_event` hands every `GDK_SCROLL` of the chain to the grab
widget (the sub) with its original, un-rebased coordinates [source] (`gtkmain.c:1789-1792`, 1923-1924), while
GDK Wayland sends the axis to the surface under the pointer only (`pointer_handle_axis` → `create_scroll_event`,
`gdkdevice-wayland.c:1929, 1527`). The sub therefore sees and consumes wheel events raised over main, so the
main list never scrolls, on X11 and Wayland alike. A hit test against `wxGetMousePosition()` cannot replace the
window test: global coordinates are unreliable on Wayland (`mouse-keyboard-focus.md`). `DropDown` connects a
`GtkWidget::event` handler on the sub's `GtkWindow` (its `m_widget`), which is emitted before wx's
`::scroll-event` handler on the same widget and suppresses it when it returns TRUE (`gtkwidget.c:7696`), then
delivers the event with `gtk_propagate_event(main->GetConnectWidget(), event)`. Returning TRUE there also stops
the parent-chain propagation.
```cpp
// Wrong: emitted on the popup's outer GtkWindow, where wx has no handlers at all
g_signal_emit_by_name(main->m_widget, "scroll-event", event, &handled);
// Right (wxGTK): replay GTK's own delivery on the widget wx listens to
gtk_propagate_event(main->GetConnectWidget(), event);
```
**Where wx listens [source].** `wxPopupWindow::Create` builds a popup from two widgets: `m_widget` is the outer
`GtkWindow` and `m_wxwindow` is a `wxPizza` child added to it. `wxWindowGTK::GetConnectWidget()` is
`m_wxwindow ? m_wxwindow : m_widget`, and `ConnectWidget()` — which binds `button_press_event`, `motion_notify_event`
and `scroll_event` — is called on that widget (`src/gtk/window.cpp` `Create`, `GetConnectWidget`). A signal emitted
on the outer `GtkWindow` therefore reaches no wx handler; only events GTK delivers to the inner widget, or to a
descendant of the grab widget, are seen by wx. `GTKGetConnectWindow()`/`GTKGetMainWindow()` are the matching
GdkWindows.
Cite: `DropDown::messureSize`, `DropDown::mouseWheelMoved`.
- **Rule:** When shrinking a dropdown to fit above the display bottom, give up if fewer than two rows would
remain.
**Why:** On Wayland the popup can land where almost no vertical space is left. Clamping to that sliver made an
-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
+12 -9
View File
@@ -5,9 +5,9 @@ on:
branches:
- main
- release/*
- belt-printer
paths:
- 'deps/**'
- 'deps_src/**'
- 'src/**'
- '**/CMakeLists.txt'
- 'version.inc'
@@ -15,10 +15,8 @@ on:
- 'resources/**'
- ".github/workflows/build_*.yml"
- ".github/workflows/unit_tests*.yml"
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
@@ -57,9 +55,7 @@ on:
default: false
concurrency:
# cancel_closed_pr_builds.yml joins this group by name, so update that file
# with any change here.
group: build-all-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
# Pushes don't cancel a running build, because only a finished branch build
# saves caches every pull request can restore.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
@@ -266,6 +262,9 @@ jobs:
date:
ver:
ver_pure:
# Belt-printer nightlies share the main nightly release but carry a `_belt`
# suffix so they never overwrite the main assets.
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: "Remove unneeded stuff to free disk space"
run:
@@ -440,11 +439,15 @@ jobs:
name: 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
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
uses: ./.github/actions/deploy-nightly
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer')
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# The asset is /app (the exes link it at runtime) plus the build tree
# slimmed to what ctest needs.
- name: Package flatpak test asset
+73 -198
View File
@@ -1,198 +1,73 @@
name: Check Cache
on:
workflow_call:
inputs:
os:
required: true
type: string
arch:
required: false
type: string
compiler:
required: false
type: string
default: msvc
build-deps-only:
required: false
type: boolean
force-build:
required: false
type: boolean
# Read by scripts/ci_deps_cache.sh.
env:
REPO: ${{ github.repository }}
WORKFLOW_REF: ${{ github.workflow_ref }}
BASE_REF: ${{ github.base_ref }}
jobs:
check_cache: # determines if there is a cache and outputs variables used in caching process
name: Check Cache
# Hosted macOS runners are scarce, so their lookup runs on Linux.
runs-on: ${{ startsWith(inputs.os, 'macos-') && 'ubuntu-24.04' || inputs.os }}
outputs:
cache-key: ${{ steps.set_outputs.outputs.cache-key }}
cache-path: ${{ steps.set_outputs.outputs.cache-path }}
valid-cache: ${{ steps.cache_deps.outputs.cache-hit }}
macos-path: ${{ steps.macos_path.outputs.path }}
key-inputs: ${{ steps.wait_check.outputs.key-inputs }}
wait-for: ${{ steps.wait_check.outputs.wait-for }}
wait-since: ${{ steps.wait_check.outputs.wait-since }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
# The wait check compares the merge commit with its first parent.
fetch-depth: ${{ startsWith(inputs.os, 'macos-') && 2 || 1 }}
# Fetches only deps/, which the key hashes, and the wait script.
sparse-checkout: |
/deps/
/scripts/ci_deps_cache.sh
sparse-checkout-cone-mode: false
- name: set outputs
id: set_outputs
env:
# Anything that changes how the tree is built belongs in the key, or a job
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
# 'linux-clang' keeps the cache it already has.
cache-os: ${{ (runner.os == 'macOS' || startsWith(inputs.os, 'macos-')) && format('{0}-{1}', inputs.os, inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
# The Windows ARM64 deps build in build-arm64, all others under build;
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
dep-folder-name: ${{ (runner.os == 'macOS' || startsWith(inputs.os, 'macos-')) && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
run: |
echo cache-key=${{ env.cache-os }}-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} >> ${{ env.output-cmd }}
# Relative to the workspace, which differs from the build's when this job runs on Linux.
echo cache-path=deps/build${{ env.dep-folder-name }} >> ${{ env.output-cmd }}
# actions/cache matches the path too, so a Linux lookup for macOS uses the
# workspace of a hosted macOS job.
- name: macOS cache path
id: macos_path
if: ${{ startsWith(inputs.os, 'macos-') }}
run: echo "path=/Users/runner/work/${GITHUB_REPOSITORY#*/}/${GITHUB_REPOSITORY#*/}/${{ steps.set_outputs.outputs.cache-path }}" >> "$GITHUB_OUTPUT"
- name: load cache
id: cache_deps
uses: actions/cache@v6
with:
path: ${{ steps.macos_path.outputs.path || format('{0}/{1}', github.workspace, steps.set_outputs.outputs.cache-path) }}
key: ${{ steps.set_outputs.outputs.cache-key }}
lookup-only: true
# A pull request that misses the cache can wait for a base branch build of the same deps.
- name: check for a base branch build to wait for
id: wait_check
if: ${{ startsWith(inputs.os, 'macos-') && github.event_name == 'pull_request' && steps.cache_deps.outputs.cache-hit != 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: bash scripts/ci_deps_cache.sh check
wait_for_deps:
name: Wait for base branch deps
needs: [check_cache]
if: ${{ needs.check_cache.outputs.wait-for != '' }}
runs-on: ubuntu-24.04
env:
WAIT_MINUTES: 120
# Must exceed WAIT_MINUTES.
timeout-minutes: 130
# Waiting runs queue here, and only the first one holds a runner.
concurrency:
group: deps-wait-${{ github.base_ref }}-${{ needs.check_cache.outputs.cache-key }}
queue: max
outputs:
hit: ${{ steps.lookup.outputs.cache-hit }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
sparse-checkout: scripts/ci_deps_cache.sh
sparse-checkout-cone-mode: false
- name: wait for the deps cache
env:
GH_TOKEN: ${{ github.token }}
KEY: ${{ needs.check_cache.outputs.cache-key }}
KEY_INPUTS: ${{ needs.check_cache.outputs.key-inputs }}
WAIT_FOR: ${{ needs.check_cache.outputs.wait-for }}
WAIT_SINCE: ${{ needs.check_cache.outputs.wait-since }}
ARCH: ${{ inputs.arch }}
run: bash scripts/ci_deps_cache.sh wait
- name: load cache
id: lookup
uses: actions/cache@v6
with:
path: ${{ needs.check_cache.outputs.macos-path }}
key: ${{ needs.check_cache.outputs.cache-key }}
lookup-only: true
# Hosted macOS runners are scarce, so a pull request's macOS build waits until
# one is free that push and nightly builds do not need. See the script.
macos_admission:
name: Wait for a macOS runner
needs: [check_cache, wait_for_deps]
if: ${{ !cancelled() && needs.check_cache.result == 'success' && startsWith(inputs.os, 'macos-') && github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'macos-priority') }}
runs-on: ubuntu-24.04
env:
WAIT_MINUTES: 240
PRIORITY: false
# Must exceed WAIT_MINUTES.
timeout-minutes: 250
# One line for every arch of every pull request, and only its first job polls.
concurrency:
group: macos-admission
queue: max
steps: &macos_admission_steps
- name: Checkout
uses: actions/checkout@v7
with:
sparse-checkout: scripts/ci_macos_admission.py
sparse-checkout-cone-mode: false
- name: wait for a macOS runner
env:
GH_TOKEN: ${{ github.token }}
MACOS_RUNNER_LIMIT: ${{ vars.MACOS_RUNNER_LIMIT }}
# -u, or the log shows nothing until the wait ends.
run: python3 -u scripts/ci_macos_admission.py
# The macos-priority label, when the run starts, puts a pull request in its own
# line, which the normal one yields to. The names are fixed, because a skipped
# job is listed under its unevaluated name, and the script matches both.
macos_admission_priority:
name: Wait for a macOS runner (priority)
needs: [check_cache, wait_for_deps]
if: ${{ !cancelled() && needs.check_cache.result == 'success' && startsWith(inputs.os, 'macos-') && github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'macos-priority') }}
runs-on: ubuntu-24.04
env:
WAIT_MINUTES: 240
PRIORITY: true
# Must exceed WAIT_MINUTES.
timeout-minutes: 250
concurrency:
group: macos-admission-priority
queue: max
steps: *macos_admission_steps
build_deps: # call next step
name: Build Deps
needs: [check_cache, wait_for_deps, macos_admission, macos_admission_priority]
# A failed wait counts as no cache, so the deps are built here. A failed
# admission still builds rather than skip macOS.
if: ${{ !cancelled() && needs.check_cache.result == 'success' }}
uses: ./.github/workflows/build_deps.yml
with:
cache-key: ${{ needs.check_cache.outputs.cache-key }}
cache-path: ${{ needs.check_cache.outputs.cache-path }}
valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' || needs.wait_for_deps.outputs.hit == 'true' }}
os: ${{ inputs.os }}
arch: ${{ inputs.arch }}
compiler: ${{ inputs.compiler }}
build-deps-only: ${{ inputs.build-deps-only }}
force-build: ${{ inputs.force-build }}
secrets: inherit
name: Check Cache
on:
workflow_call:
inputs:
os:
required: true
type: string
arch:
required: false
type: string
compiler:
required: false
type: string
default: msvc
build-deps-only:
required: false
type: boolean
force-build:
required: false
type: boolean
jobs:
check_cache: # determines if there is a cache and outputs variables used in caching process
name: Check Cache
runs-on: ${{ inputs.os }}
outputs:
cache-key: ${{ steps.set_outputs.outputs.cache-key }}
cache-path: ${{ steps.set_outputs.outputs.cache-path }}
valid-cache: ${{ steps.cache_deps.outputs.cache-hit }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
- name: set outputs
id: set_outputs
env:
# Anything that changes how the tree is built belongs in the key, or a job
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
# 'linux-clang' keeps the cache it already has.
cache-os: ${{ runner.os == 'macOS' && format('{0}-{1}', inputs.os, inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
# The Windows ARM64 deps build in build-arm64, all others under build;
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
run: |
echo cache-key=${{ env.cache-os }}-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} >> ${{ env.output-cmd }}
echo cache-path=${{ github.workspace }}/deps/build${{ env.dep-folder-name }} >> ${{ env.output-cmd }}
- name: load cache
id: cache_deps
uses: actions/cache@v6
with:
path: ${{ steps.set_outputs.outputs.cache-path }}
key: ${{ steps.set_outputs.outputs.cache-key }}
lookup-only: true
build_deps: # call next step
name: Build Deps
needs: [check_cache]
uses: ./.github/workflows/build_deps.yml
with:
cache-key: ${{ needs.check_cache.outputs.cache-key }}
cache-path: ${{ needs.check_cache.outputs.cache-path }}
valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' }}
os: ${{ inputs.os }}
arch: ${{ inputs.arch }}
compiler: ${{ inputs.compiler }}
build-deps-only: ${{ inputs.build-deps-only }}
force-build: ${{ inputs.force-build }}
secrets: inherit
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
- name: load cached deps
uses: actions/cache@v6
with:
path: ${{ github.workspace }}/${{ inputs.cache-path }}
path: ${{ inputs.cache-path }}
key: ${{ inputs.cache-key }}
- uses: lukka/get-cmake@latest
@@ -122,7 +122,7 @@ jobs:
choco install strawberryperl
}
# cache-path is the install directory inside the deps build directory.
$deps = (Split-Path "${{ github.workspace }}/${{ inputs.cache-path }}").Replace('\', '/')
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
.\build_win.bat -d --arch ${{ inputs.arch }} --deps-dir $deps @flags
+75 -38
View File
@@ -33,6 +33,11 @@ jobs:
ubuntu-ver: '2404'
ubuntu-ver-str: '_Ubuntu2404'
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
# Branches whose builds are published to the nightly release. The
# belt-printer branch ships alongside main but its assets carry a `_belt`
# suffix (nightly_suffix) so they never overwrite the main nightly assets.
deploy_nightly: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' }}
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: Checkout
@@ -44,7 +49,7 @@ jobs:
if: ${{ !(runner.os == 'macOS' && inputs.macos-combine-only) }}
uses: actions/cache@v6
with:
path: ${{ github.workspace }}/${{ inputs.cache-path }}
path: ${{ inputs.cache-path }}
key: ${{ inputs.cache-key }}
fail-on-cache-miss: true
@@ -179,13 +184,6 @@ jobs:
if: runner.os == 'macOS' && !inputs.macos-combine-only && !vars.SELF_HOSTED
run: |
df -hI /
# Deleting the unused Xcodes takes minutes, so only do it when the
# build might not fit. A cold build uses under 4 GiB after this step.
avail_gib=$(df -g / | awk 'NR == 2 { print $4 }')
if [ "$avail_gib" -ge 15 ]; then
echo "$avail_gib GiB free, enough for the build"
exit 0
fi
# Keep only the selected Xcode
xcode=$(basename "$(cd "$(xcode-select -p)/../.." && pwd -P)")
sudo find /Applications -maxdepth 1 -type d -name "Xcode_*.app" ! -name "$xcode" -exec rm -rf {} +
@@ -262,7 +260,7 @@ jobs:
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
timeout-minutes: 30
working-directory: ${{ github.workspace }}
env:
@@ -341,16 +339,16 @@ jobs:
# Notarize main DMG
xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
retry xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Notarize profile validator DMG if it exists
if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then
xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
retry xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
- name: Create DMG without notary
if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/belt-printer' && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
@@ -396,19 +394,26 @@ jobs:
if-no-files-found: ignore
- name: Deploy Mac release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_Mac_universal_nightly.dmg
asset_name: OrcaSlicer_Mac_universal_nightly${{ env.nightly_suffix }}.dmg
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# The DMG is only built when the validator app was.
- name: Deploy Mac OrcaSlicer_profile_validator DMG release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED && hashFiles(format('OrcaSlicer_profile_validator_Mac_universal_{0}.dmg', env.ver)) != ''
uses: ./.github/actions/deploy-nightly
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_profile_validator_Mac_universal_nightly.dmg
asset_content_type: application/octet-stream
max_releases: 1
# Windows
- name: Set Windows build variables
@@ -453,10 +458,14 @@ jobs:
- name: Build slicer Win
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
# Orca: Removed Netfabb STL fixing service support in favor of CGAL.
# env:
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
# WindowsSDKVersion: '10.0.26100.0\'
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
run: |
# cache-path is the install directory inside the deps build directory.
$deps = (Split-Path "${{ github.workspace }}/${{ inputs.cache-path }}").Replace('\', '/')
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
.\build_win.bat -s --tests -i --arch ${{ inputs.arch }} --build-dir $env:BUILD_DIR --deps-dir $deps @flags
@@ -534,28 +543,37 @@ jobs:
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip
content_type: application/x-zip-compressed
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}_portable.zip
asset_content_type: application/x-zip-compressed
max_releases: 1
- name: Deploy Windows release installer
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe
content_type: application/x-msdownload
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}.exe
asset_content_type: application/x-msdownload
max_releases: 1
- name: Deploy Windows OrcaSlicer_profile_validator release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
asset_name: OrcaSlicer_profile_validator_Windows_nightly.exe
content_type: application/x-msdownload
asset_content_type: application/x-msdownload
max_releases: 1
- name: Build MSIX Store package Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
@@ -683,37 +701,56 @@ jobs:
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: ./.github/actions/deploy-nightly
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./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${{ env.nightly_suffix }}.AppImage
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
run: |
git -c user.name="${GITHUB_ACTOR}" -c user.email="${GITHUB_ACTOR}@users.noreply.github.com" tag -f -a nightly-builds "${GITHUB_SHA}" -m nightly-builds
git push -f origin refs/tags/nightly-builds
uses: rickstaa/action-create-tag@v1
with:
tag: "nightly-builds"
tag_exists_error: false
force_push_tag: true
message: "nightly-builds"
- name: Deploy Ubuntu OrcaSlicer_profile_validator release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: ./.github/actions/deploy-nightly
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/src/Release/OrcaSlicer_profile_validator
asset_name: OrcaSlicer_profile_validator_Linux${{ env.ubuntu-ver-str }}_nightly
asset_content_type: application/octet-stream
max_releases: 1
- name: Deploy Ubuntu generate_system_cache release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: ./.github/actions/deploy-nightly
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/src/dev-utils/Release/generate_system_cache
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
asset_content_type: application/octet-stream
max_releases: 1
- name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: ./.github/actions/deploy-nightly
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/resources/profiles/orca_custom_preset_tests.zip
asset_name: orca_custom_preset_tests.zip
asset_content_type: application/octet-stream
max_releases: 1
# The build has just touched everything it can use, so an object
# untouched for a week is dead, usually orphaned by a flag change.
@@ -1,21 +0,0 @@
name: Cancel builds of closed pull requests
on:
# pull_request_target needs no approval for first-time contributors and must never
# check out pull request code.
pull_request_target:
types:
- closed
permissions: {}
jobs:
cancel:
# Matches the concurrency group in build_all.yml, so joining it cancels the pull
# request's unfinished Build all run.
concurrency:
group: build-all-pull_request-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- run: echo "Joined concurrency group build-all-pull_request-${{ github.event.pull_request.number }}."
+2 -43
View File
@@ -13,7 +13,6 @@ on:
# both it and its tests live in scripts/, so a PR touching only those must still run
# this workflow.
- 'resources/printers/**'
- 'resources/info/legacy_compatible_printers.json'
- 'scripts/**'
# orca_profile_tool.py reads the variant key sets from PrintConfig.cpp, and its
# tests the obsolete keys, so a PR changing either must be checked against the profiles.
@@ -60,21 +59,6 @@ jobs:
python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log
exit ${PIPESTATUS[0]}
# The app migrates user filament presets made against 2.4.2 back to the printers their
# parent listed then, from resources/info/legacy_compatible_printers.json. A PR that
# renames a filament preset or narrows its compatible_printers changes what that map
# must say, so it has to regenerate it.
- name: Check the legacy compatible printers map
id: legacy_compat
continue-on-error: true
run: |
set -o pipefail
base="${{ runner.temp }}/v2.4.2"
git fetch --no-tags --depth=1 origin refs/tags/v2.4.2:refs/tags/v2.4.2
mkdir -p "$base"
git archive v2.4.2 resources/profiles | tar -x -C "$base"
python3 ./scripts/orca_profile_tool.py legacy-compat --base "$base/resources/profiles" --check 2>&1 | tee ${{ runner.temp }}/legacy_compat.log
# download
- name: Download
working-directory: ${{ github.workspace }}
@@ -90,26 +74,12 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
# The validator above is the nightly build of main, so it cannot slice profiles that use
# settings a PR adds to the engine: it reports their placeholders as undefined. A PR that
# changes src/ also runs Build all, whose Slice check runs this same sweep with the
# validator built from the PR, so the sweep below only runs for the other PRs.
- name: Detect engine changes
id: engine_changes
if: ${{ github.event_name == 'pull_request' }}
run: |
base=${{ github.event.pull_request.base.sha }}
if git fetch --no-tags --depth=1 origin "$base" && ! git diff --quiet "$base" HEAD -- src/; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "::notice::This PR changes src/, so Build all's Slice check slices the profiles with the PR-built validator."
fi
# Slice a two-colour cube through every printer, and through every system process/filament whose
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
# invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code)
id: validate_slice
if: ${{ steps.engine_changes.outputs.changed != 'true' }}
continue-on-error: true
run: |
set +e
@@ -233,7 +203,7 @@ jobs:
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
{
# Marker matched by check_profiles_comment.yml to delete prior comments.
@@ -250,17 +220,6 @@ jobs:
echo ""
fi
if [ "${{ steps.legacy_compat.outcome }}" = "failure" ]; then
echo "### Legacy Compatible Printers Map Out of Date"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/legacy_compat.log || echo "No output captured"
echo '```'
echo ""
echo 'Regenerate it: `git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2`, then `python3 scripts/orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles`.'
echo ""
fi
if [ "${{ steps.validate_system.outcome }}" = "failure" ]; then
echo "### System Profile Validation Failed"
echo ""
@@ -310,7 +269,7 @@ jobs:
retention-days: 1
- name: Fail if any check failed
if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
echo "One or more profile checks failed; see the step logs above."
echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).'
+4 -70
View File
@@ -16,15 +16,6 @@ on:
permissions:
contents: read
# Listing caches and builds while waiting for a deps cache.
actions: read
# Read by scripts/ci_deps_cache.sh.
env:
REPO: ${{ github.repository }}
WORKFLOW: build_all.yml
BASE_REF: ${{ github.base_ref }}
JOB: build_linux (${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }})
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -44,7 +35,7 @@ jobs:
# The PR merge commit plus its first parent, the base it is diffed against.
fetch-depth: 2
- name: Look for changed files
- name: Look for changed C++ files
id: changes
run: |
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
@@ -52,75 +43,18 @@ jobs:
else
echo "No C++ changes under src/ or tests/."
fi
if ! git diff --quiet HEAD^1 -- deps; then
echo "deps=true" >> "$GITHUB_OUTPUT"
fi
echo "deps-key=linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}" >> "$GITHUB_OUTPUT"
# Parsing needs the dependency headers, not a build of this PR's deps/.
# Without an exact match the job waits for the build that saves it: this
# PR's own when it changes deps/, otherwise a running push build of the
# base branch with the same deps. With neither, a PR that keeps deps/
# falls back to the newest cache.
# Parsing needs the dependency headers, not a build of this PR's deps/, so
# a PR that changes deps/ falls back to the newest cache main has.
- name: Restore cached deps
id: deps_cache
if: steps.changes.outputs.cpp == 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
- name: Look for a build to wait for
id: wait_check
if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }}
# Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The
# wait ends early when the deps job fails.
- name: Wait for the deps cache
if: steps.wait_check.outputs.wait-for != ''
timeout-minutes: 100
env:
GH_TOKEN: ${{ github.token }}
KEY: ${{ steps.changes.outputs.deps-key }}
CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }}
KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }}
WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }}
WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }}
WAIT_MINUTES: 90
run: bash scripts/ci_deps_cache.sh wait
- name: Restore the deps cache after waiting
id: waited_cache
if: steps.wait_check.outputs.wait-for != ''
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
- name: Fail without this PR's deps cache
if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
run: |
echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy."
exit 1
- name: Restore the newest cached deps
id: newest_cache
if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
restore-keys: linux-clang-cache-orcaslicer_deps-build-
fail-on-cache-miss: true
- name: Warn about older deps
if: steps.newest_cache.outcome == 'success' && steps.newest_cache.outputs.cache-hit != 'true'
run: echo "::warning title=Older deps::No cache matches this PR's deps/, so clang-tidy parsed against ${{ steps.newest_cache.outputs.cache-matched-key }}. If it reports errors in code that uses a dependency, rerun clang-tidy once the base branch's Build all has saved the new deps."
- name: Apt-Install Dependencies
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps
+2 -2
View File
@@ -156,7 +156,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libopengl0 libgl1 libegl1 libwebkit2gtk-4.1-0
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- uses: actions/setup-python@v6
with:
@@ -224,7 +224,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libgl1 libegl1 libwebkit2gtk-4.1-0
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness
run: |
-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.
- Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped.
- For new or changed UI strings, check they follow [Writing translatable strings in the source](#writing-translatable-strings-in-the-source): no concatenated fragments, plural forms for counts, contexts for ambiguous words.
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) for that language.
## Localization & translations
@@ -75,19 +74,6 @@ ctest --test-dir ./tests/fff_print -C Release
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md) for the human-facing version of these principles.
### Writing translatable strings in the source
Translators see one msgid at a time and cannot change the code around it, so each message must be complete in a single msgid. Use the wrapper that fits instead of assembling text from pieces:
- **One msgid per sentence, with placeholders.** Never build a message by concatenating translated fragments (`_L("A") + name + _L(" is B")`) or by splicing a translated word such as `_L("left")` into another sentence: the translator can't reorder or inflect the parts. Write `format(_L("%1% is too close to others"), name)`.
- **A count needs a plural form.** Don't write `file(s)` or leave a verb unchanged after a number. Use `_L_PLURAL("%1% object", "%1% objects", n)`. libslic3r has no plural lookup, so word the message so it doesn't depend on a count, or build it in the GUI.
- **An ambiguous word needs a context.** When a short string means different things in different places (verb or noun, a mode name, a CAD term), give each sense its own context with `_L_CONTEXT`/`_u8L_CONTEXT`/`L_CONTEXT`. Keep the most common sense without a context so its existing translations stay valid.
- **Mark every user-visible string** (`_L`/`_u8L`, or `L` where it is translated later) and list new source files in `localization/i18n/list.txt`: files that aren't listed are never extracted.
- Add a `// TRN` comment above a new string whose meaning or placeholders aren't obvious.
- Never choose behavior by comparing translated text; compare ids or flags.
See `.claude/skills/orca-translations/references/source-audit.md` for the full checklist.
### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
+43 -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
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
# if(WIN32)
# if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
# endif()
# endif()
# if(DEFINED WIN10SDK_PATH)
# #BBS: modify win10sdk_path
# if (EXISTS "${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h")
# set(WIN10SDK_INCLUDE_PATH "${WIN10SDK_PATH}")
# else()
# message("WIN10SDK_PATH is invalid: ${WIN10SDK_PATH}")
# message("${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_PATH)
# endif()
# else()
# # Try to use the default Windows 10 SDK path.
# if (DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKVersion})
# set(WIN10SDK_INCLUDE_PATH "$ENV{WindowsSdkDir}/Include/$ENV{WindowsSDKVersion}")
# else ()
# set(WIN10SDK_INCLUDE_PATH "C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0")
# endif ()
# if (NOT EXISTS "${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h")
# message("${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_INCLUDE_PATH)
# endif()
# endif()
# if(WIN10SDK_INCLUDE_PATH)
# message("Building with Win10 Netfabb STL fixing service support")
# add_definitions(-DHAS_WIN10SDK)
# include_directories(SYSTEM "${WIN10SDK_INCLUDE_PATH}")
# else()
# message("Building without Win10 Netfabb STL fixing service support")
# endif()
# endif()
if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")
if (CMAKE_OSX_DEPLOYMENT_TARGET)
@@ -1043,10 +1084,10 @@ endif ()
find_path(SPNAV_INCLUDE_DIR spnav.h)
if (SPNAV_INCLUDE_DIR)
find_library(SPNAV_LIB NAMES libspnav.a spnav)
find_library(SPNAV_LIB NAMES libspnav.a) # Force linking libspnav statically
if (SPNAV_LIB)
add_definitions(-DHAVE_SPNAV)
message(STATUS "SPNAV library found: ${SPNAV_LIB}")
message(STATUS "SPNAV library found")
else()
message(STATUS "SPNAV library NOT found, Spacenavd not supported")
endif()
+11 -4
View File
@@ -68,10 +68,6 @@ If you come across any of these in search results, please <b>report them</b> as
Regular updates fueled by continuous community contributions.
- **Wide Printer Compatibility**
Supports a broad range of printers: Bambu Lab, Prusa, Creality, Voron, and more.
- **[Belt Printer Support](https://www.orcaslicer.com/wiki/belt_printing)**
Slice for belt / conveyor (infinite-Z) printers, with belt-aware supports and a tilted-bed preview. Contributed by [Joseph Robertson (@HarrierPigeon)](https://github.com/HarrierPigeon).
- **[IDEX/IQEX Parallel Printing Support](https://www.orcaslicer.com/wiki/idex_iqex_parallel_printing)**
Print copies or mirror images of a part on every carriage of an IDEX or IQEX printer at once, with the mode chosen per plate and nozzle clearance zones shown on the bed. Contributed by [Clifford (@cgarwood82)](https://github.com/cgarwood82).
- Additional features can be found in the [change notes](https://github.com/OrcaSlicer/OrcaSlicer/releases/).
# Wiki
@@ -93,6 +89,17 @@ Visit our GitHub Releases page for the latest stable version of OrcaSlicer, reco
🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)**
Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated.
### Belt Printer Builds
The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix:
- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`)
- **Belt** — `_belt` suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly_belt.exe`)
The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview.
> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998).
# How to install
## Windows
+5 -57
View File
@@ -8,10 +8,9 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null
function usage() {
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-J N][-p][-r][-s][-t][-u][-l][-L] [-- CMAKE_ARGS...]"
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]"
echo " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (where possible)"
echo " -J N: build up to N dependencies at a time, each still using -j jobs (default: 1)"
echo " -b: build in Debug mode"
echo " -c: force a clean build"
echo " -C: enable ANSI-colored compile output (GNU/Clang only)"
@@ -29,25 +28,20 @@ function usage() {
echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
echo " -l: use Clang instead of GCC (default: GCC)"
echo " -L: use ld.lld as linker (if available)"
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and OrcaSlicer (-s/-t) builds,"
echo " e.g. -DCMAKE_CXX_FLAGS=\"-O2 -march=native\", -DCMAKE_CXX_COMPILER=g++-14, -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=mold."
echo " They come last on the cmake command line, so they override what -l and -L set."
echo "For a first use, you want to './${SCRIPT_NAME} -u'"
echo " and then './${SCRIPT_NAME} -dsi'"
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
echo "Use './${SCRIPT_NAME} -gF -istrlL' to rebuild the cached runner image first."
echo "Set ORCA_CONTAINER_CLI, ORCA_DOCKER_IMAGE, ORCA_DOCKER_BASE_IMAGE, or ORCA_DOCKER_CMAKE_VERSION to override the container runtime, cached image tag, base image, or CMake version."
echo "DEPS_EXTRA_BUILD_ARGS and ORCA_EXTRA_BUILD_ARGS add whitespace-separated cmake configure arguments to the deps and OrcaSlicer builds; prefer '--' for arguments that contain spaces."
}
SLIC3R_PRECOMPILED_HEADERS="ON"
DEPS_PARALLEL=""
unset name
BUILD_DIR=build
BUILD_CONFIG=Release
FORWARDED_ARGS=()
while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
while getopts ":1j:bcCdDeFghiprstulL" opt ; do
case ${opt} in
1 )
export CMAKE_BUILD_PARALLEL_LEVEL=1
@@ -57,10 +51,6 @@ while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
export CMAKE_BUILD_PARALLEL_LEVEL=$OPTARG
FORWARDED_ARGS+=("-j" "$OPTARG")
;;
J )
DEPS_PARALLEL=$OPTARG
FORWARDED_ARGS+=("-J" "$OPTARG")
;;
b )
BUILD_DIR=build-dbg
BUILD_CONFIG=Debug
@@ -135,20 +125,6 @@ while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
esac
done
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
if [[ ${OPTIND} -le $# ]] ; then
separator_index=$((OPTIND-1))
if [[ "${!separator_index}" != "--" ]] ; then
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
exit 1
fi
fi
shift $((OPTIND-1))
EXTRA_CMAKE_ARGS=("$@")
if [[ ${#EXTRA_CMAKE_ARGS[@]} -gt 0 ]] ; then
FORWARDED_ARGS+=("--" "${EXTRA_CMAKE_ARGS[@]}")
fi
if [ ${OPTIND} -eq 1 ] ; then
usage
exit 1
@@ -159,11 +135,6 @@ if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
exit 1
fi
if [[ -n "${DEPS_PARALLEL}" ]] && ! [[ "${DEPS_PARALLEL}" =~ ^[1-9][0-9]*$ ]] ; then
echo "Error: -J expects a positive integer."
exit 1
fi
function check_available_memory_and_disk() {
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
MIN_MEM_GB=10
@@ -565,30 +536,8 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}" "${EXTRA_CMAKE_ARGS[@]}"
# The top-level build runs one dependency at a time by default, which keeps the console
# output readable and lets that dependency's own build use all of CMAKE_BUILD_PARALLEL_LEVEL.
# -J raises the top level instead, and -j still applies in full to every dependency, so the
# worst case is -J times -j compile jobs at once. Ninja has no job server to share a pool
# across the nested builds, so that ceiling is not enforced anywhere: pick -J to suit the RAM.
DEPS_JOBS=1
if [[ -n "${DEPS_PARALLEL}" ]] ; then
DEPS_JOBS=${DEPS_PARALLEL}
SAVED_PARALLEL_LEVEL=${CMAKE_BUILD_PARALLEL_LEVEL-}
export CMAKE_BUILD_PARALLEL_LEVEL=${CMAKE_BUILD_PARALLEL_LEVEL:-$(nproc)}
echo "Building up to ${DEPS_JOBS} dependencies at a time, ${CMAKE_BUILD_PARALLEL_LEVEL} jobs each: up to $(( DEPS_JOBS * CMAKE_BUILD_PARALLEL_LEVEL )) compile jobs at once."
fi
print_and_run cmake --build deps/$BUILD_DIR -j"${DEPS_JOBS}"
if [[ -n "${DEPS_PARALLEL}" ]] ; then
# Give the whole -j back to the OrcaSlicer build below.
if [[ -n "${SAVED_PARALLEL_LEVEL}" ]] ; then
export CMAKE_BUILD_PARALLEL_LEVEL=${SAVED_PARALLEL_LEVEL}
else
unset CMAKE_BUILD_PARALLEL_LEVEL
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[@]}"
print_and_run cmake --build deps/$BUILD_DIR -j1
fi
if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
@@ -611,8 +560,7 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
-DORCA_TOOLS=ON \
"${COLORED_OUTPUT}" \
"${BUILD_ARGS[@]}" \
"${EXTRA_CMAKE_ARGS[@]}"
"${BUILD_ARGS[@]}"
echo "done"
if [[ -n "${BUILD_ORCA}" ]]; then
echo "Building OrcaSlicer ..."
+2 -2
View File
@@ -104,8 +104,8 @@ fi
CMAKE_VERSION=$(cmake --version | head -1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
if [ "$CMAKE_VERSION" -ge 4 ] 2>/dev/null; then
export CMAKE_POLICY_VERSION_MINIMUM=3.10
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.10"
export CMAKE_POLICY_VERSION_MINIMUM=3.5
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
echo "Detected CMake 4.x, adding compatibility flag (env + cmake arg)"
else
export CMAKE_POLICY_COMPAT=""
-8
View File
@@ -1,18 +1,10 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
# no headers for that object. ccache reproduces /showIncludes output on a hit.
#
# Clang also records the modification time of every input in a precompiled header
# and rejects the header once one differs. ccache caches the compile that creates
# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
# with the old file's time, and every compile that uses it fails. The build system
# already rebuilds the header when an input changes. This covers the dependencies,
# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif ()
endforeach ()
+359
View File
@@ -0,0 +1,359 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
# PrusaSlicer specifics:
# This file is backported from CMake 3.15 distribution to behave uniformly
# across all versions of CMake. It explicitly adds GLEW_STATIC compile
# definition to static targets which is needed to prevent link errors.
#[=======================================================================[.rst:
FindGLEW
--------
Find the OpenGL Extension Wrangler Library (GLEW)
Input Variables
^^^^^^^^^^^^^^^
The following variables may be set to influence this module’s behavior:
``GLEW_USE_STATIC_LIBS``
to find and create :prop_tgt:`IMPORTED` target for static linkage.
``GLEW_VERBOSE``
to output a detailed log of this module.
Imported Targets
^^^^^^^^^^^^^^^^
This module defines the following :ref:`Imported Targets <Imported Targets>`:
``GLEW::glew``
The GLEW shared library.
``GLEW::glew_s``
The GLEW static library, if ``GLEW_USE_STATIC_LIBS`` is set to ``TRUE``.
``GLEW::GLEW``
Duplicates either ``GLEW::glew`` or ``GLEW::glew_s`` based on availability.
Result Variables
^^^^^^^^^^^^^^^^
This module defines the following variables:
``GLEW_INCLUDE_DIRS``
include directories for GLEW
``GLEW_LIBRARIES``
libraries to link against GLEW
``GLEW_SHARED_LIBRARIES``
libraries to link against shared GLEW
``GLEW_STATIC_LIBRARIES``
libraries to link against static GLEW
``GLEW_FOUND``
true if GLEW has been found and can be used
``GLEW_VERSION``
GLEW version
``GLEW_VERSION_MAJOR``
GLEW major version
``GLEW_VERSION_MINOR``
GLEW minor version
``GLEW_VERSION_MICRO``
GLEW micro version
#]=======================================================================]
include(FindPackageHandleStandardArgs)
if(APPLE)
find_package(OpenGL QUIET)
if(OpenGL_FOUND)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Found OpenGL Framework.")
message(STATUS "FindGLEW: OPENGL_LIBRARIES: ${OPENGL_LIBRARIES}")
endif()
else()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: could not find GLEW library.")
endif()
return()
endif()
endif()
function(__glew_set_find_library_suffix shared_or_static)
if((UNIX AND NOT APPLE) AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".so")
elseif((UNIX AND NOT APPLE) AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
elseif(APPLE AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dylib;.so")
elseif(APPLE AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
elseif(WIN32 AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib")
elseif(WIN32 AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib;.a;.dll.a")
endif()
set(CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_FIND_LIBRARY_SUFFIXES}" PARENT_SCOPE)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: CMAKE_FIND_LIBRARY_SUFFIXES for ${shared_or_static}: ${CMAKE_FIND_LIBRARY_SUFFIXES}")
endif()
endfunction()
if(GLEW_VERBOSE)
if(DEFINED GLEW_USE_STATIC_LIBS)
message(STATUS "FindGLEW: GLEW_USE_STATIC_LIBS: ${GLEW_USE_STATIC_LIBS}.")
else()
message(STATUS "FindGLEW: GLEW_USE_STATIC_LIBS is undefined. Treated as FALSE.")
endif()
endif()
find_path(GLEW_INCLUDE_DIR GL/glew.h)
mark_as_advanced(GLEW_INCLUDE_DIR)
set(GLEW_INCLUDE_DIRS ${GLEW_INCLUDE_DIR})
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_INCLUDE_DIR: ${GLEW_INCLUDE_DIR}")
message(STATUS "FindGLEW: GLEW_INCLUDE_DIRS: ${GLEW_INCLUDE_DIRS}")
endif()
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "x64" OR "${CMAKE_GENERATOR}" MATCHES "Win64")
set(_arch "x64")
elseif("${CMAKE_GENERATOR_PLATFORM}" MATCHES "ARM64")
set(_arch "x64") # GLEW ships one header set; ARM64 uses the x64 import path
else()
set(_arch "Win32")
endif()
set(__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
__glew_set_find_library_suffix(SHARED)
find_library(GLEW_SHARED_LIBRARY_RELEASE
NAMES GLEW glew glew32
PATH_SUFFIXES lib lib64 libx32 lib/Release/${_arch}
PATHS ENV GLEW_ROOT)
find_library(GLEW_SHARED_LIBRARY_DEBUG
NAMES GLEWd glewd glew32d
PATH_SUFFIXES lib lib64
PATHS ENV GLEW_ROOT)
__glew_set_find_library_suffix(STATIC)
find_library(GLEW_STATIC_LIBRARY_RELEASE
NAMES GLEW glew glew32s
PATH_SUFFIXES lib lib64 libx32 lib/Release/${_arch}
PATHS ENV GLEW_ROOT)
find_library(GLEW_STATIC_LIBRARY_DEBUG
NAMES GLEWds GLEWd glewd glewds glew32ds
PATH_SUFFIXES lib lib64
PATHS ENV GLEW_ROOT)
set(CMAKE_FIND_LIBRARY_SUFFIXES ${__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES})
unset(__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES)
include(SelectLibraryConfigurations)
select_library_configurations(GLEW_SHARED)
select_library_configurations(GLEW_STATIC)
if(NOT GLEW_USE_STATIC_LIBS)
set(GLEW_LIBRARIES ${GLEW_SHARED_LIBRARY})
else()
set(GLEW_LIBRARIES ${GLEW_STATIC_LIBRARY})
endif()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY_RELEASE: ${GLEW_SHARED_LIBRARY_RELEASE}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY_RELEASE: ${GLEW_STATIC_LIBRARY_RELEASE}")
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY_DEBUG: ${GLEW_SHARED_LIBRARY_DEBUG}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY_DEBUG: ${GLEW_STATIC_LIBRARY_DEBUG}")
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY: ${GLEW_SHARED_LIBRARY}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY: ${GLEW_STATIC_LIBRARY}")
message(STATUS "FindGLEW: GLEW_LIBRARIES: ${GLEW_LIBRARIES}")
endif()
# Read version from GL/glew.h file
if(EXISTS "${GLEW_INCLUDE_DIR}/GL/glew.h")
file(STRINGS "${GLEW_INCLUDE_DIR}/GL/glew.h" _contents REGEX "^VERSION_.+ [0-9]+")
if(_contents)
string(REGEX REPLACE ".*VERSION_MAJOR[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MAJOR "${_contents}")
string(REGEX REPLACE ".*VERSION_MINOR[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MINOR "${_contents}")
string(REGEX REPLACE ".*VERSION_MICRO[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MICRO "${_contents}")
set(GLEW_VERSION "${GLEW_VERSION_MAJOR}.${GLEW_VERSION_MINOR}.${GLEW_VERSION_MICRO}")
endif()
endif()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_VERSION_MAJOR: ${GLEW_VERSION_MAJOR}")
message(STATUS "FindGLEW: GLEW_VERSION_MINOR: ${GLEW_VERSION_MINOR}")
message(STATUS "FindGLEW: GLEW_VERSION_MICRO: ${GLEW_VERSION_MICRO}")
message(STATUS "FindGLEW: GLEW_VERSION: ${GLEW_VERSION}")
endif()
find_package_handle_standard_args(GLEW
REQUIRED_VARS GLEW_INCLUDE_DIRS GLEW_LIBRARIES
VERSION_VAR GLEW_VERSION)
if(NOT GLEW_FOUND)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: could not find GLEW library.")
endif()
return()
endif()
if(NOT TARGET GLEW::glew AND NOT GLEW_USE_STATIC_LIBS)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::glew imported target.")
endif()
add_library(GLEW::glew UNKNOWN IMPORTED)
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(GLEW_SHARED_LIBRARY_RELEASE)
set_property(TARGET GLEW::glew
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::glew
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_SHARED_LIBRARY_RELEASE}")
endif()
if(GLEW_SHARED_LIBRARY_DEBUG)
set_property(TARGET GLEW::glew
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::glew
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_SHARED_LIBRARY_DEBUG}")
endif()
elseif(NOT TARGET GLEW::glew_s AND GLEW_USE_STATIC_LIBS)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::glew_s imported target.")
endif()
add_library(GLEW::glew_s UNKNOWN IMPORTED)
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
set_target_properties(GLEW::glew_s PROPERTIES INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(GLEW_STATIC_LIBRARY_RELEASE)
set_property(TARGET GLEW::glew_s
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::glew_s
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_STATIC_LIBRARY_RELEASE}")
endif()
if(GLEW_STATIC_LIBRARY_DEBUG)
set_property(TARGET GLEW::glew_s
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::glew_s
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_STATIC_LIBRARY_DEBUG}")
endif()
endif()
if(NOT TARGET GLEW::GLEW)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::GLEW imported target.")
endif()
add_library(GLEW::GLEW UNKNOWN IMPORTED)
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(TARGET GLEW::glew)
if(GLEW_SHARED_LIBRARY_RELEASE)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_SHARED_LIBRARY_RELEASE}")
endif()
if(GLEW_SHARED_LIBRARY_DEBUG)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_SHARED_LIBRARY_DEBUG}")
endif()
elseif(TARGET GLEW::glew_s)
if(GLEW_STATIC_LIBRARY_RELEASE)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_STATIC_LIBRARY_RELEASE}"
INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
endif()
if(GLEW_STATIC_LIBRARY_DEBUG AND GLEW_USE_STATIC_LIBS)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_STATIC_LIBRARY_DEBUG}"
INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
endif()
elseif(GLEW_VERBOSE)
message(WARNING "FindGLEW: no `GLEW::glew` or `GLEW::glew_s` target was created. Something went wrong in FindGLEW target creation.")
endif()
endif()
+7 -4
View File
@@ -1,5 +1,5 @@
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0")
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "" FORCE)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
endif()
#
@@ -24,7 +24,7 @@ endif()
# therefore, unfortunately, the installation cannot be copied/moved elsewhere without re-installing wxWidgets.
#
cmake_minimum_required(VERSION 3.10)
cmake_minimum_required(VERSION 3.2)
if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
@@ -224,7 +224,7 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_MODULE_PATH:STRING=${PROJECT_SOURCE_DIR}/../cmake/modules
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
@@ -279,7 +279,7 @@ else()
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
@@ -379,8 +379,10 @@ include(Boost/Boost.cmake)
include(Cereal/Cereal.cmake)
include(Qhull/Qhull.cmake)
include(GLEW/GLEW.cmake)
include(GLFW/GLFW.cmake)
include(OpenCSG/OpenCSG.cmake)
set(SLVS_PKG "")
if (SLIC3R_CAD)
include(SLVS/SLVS.cmake)
@@ -476,6 +478,7 @@ set(_dep_list
dep_Draco
dep_NLopt
dep_OpenVDB
dep_OpenCSG
${SLVS_PKG}
dep_OpenCV
dep_Eigen
+2
View File
@@ -1,4 +1,6 @@
orcaslicer_add_cmake_project(EXPAT
# GIT_REPOSITORY https://github.com/nigels-com/glew.git
# GIT_TAG 3a8eff7 # 2.1.0
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/expat
)
+14
View File
@@ -0,0 +1,14 @@
# We have to check for OpenGL to compile GLEW
set(OpenGL_GL_PREFERENCE "LEGACY") # to prevent a nasty warning by cmake
find_package(OpenGL QUIET REQUIRED)
orcaslicer_add_cmake_project(
GLEW
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/glew
CMAKE_ARGS
-DGLEW_USE_EGL=OFF
)
if (MSVC)
add_debug_dep(dep_GLEW)
endif ()
+44
View File
@@ -0,0 +1,44 @@
cmake_minimum_required(VERSION 3.0)
project(GLEW)
find_package(OpenGL REQUIRED)
# Allow parent project to control EGL usage.
# Default to OFF since OrcaSlicer forces GDK_BACKEND=x11 (using GLX contexts).
# GLEW must use glXGetProcAddressARB (GLX) to match wxWidgets GL canvas.
# Using EGL function loading with GLX contexts causes rendering failures.
option(GLEW_USE_EGL "Use EGL instead of GLX for OpenGL function loading" OFF)
if(GLEW_USE_EGL)
message(STATUS "Building GLEW with EGL support")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DGLEW_EGL")
else()
message(STATUS "Building GLEW with GLX support")
endif()
add_library(GLEW src/glew.c)
target_include_directories(GLEW PRIVATE include/)
target_link_libraries(GLEW PUBLIC OpenGL::GL)
if (NOT BUILD_SHARED_LIBS)
target_compile_definitions(GLEW PUBLIC GLEW_STATIC)
endif ()
include(GNUInstallDirs)
install(
FILES
${PROJECT_SOURCE_DIR}/include/GL/glew.h
${PROJECT_SOURCE_DIR}/include/GL/wglew.h
${PROJECT_SOURCE_DIR}/include/GL/glxew.h
${PROJECT_SOURCE_DIR}/include/GL/eglew.h
DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}/GL
)
install(TARGETS GLEW GLEW
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
+73
View File
@@ -0,0 +1,73 @@
The OpenGL Extension Wrangler Library
Copyright (C) 2002-2007, Milan Ikits <milan ikits[]ieee org>
Copyright (C) 2002-2007, Marcelo E. Magallon <mmagallo[]debian org>
Copyright (C) 2002, Lev Povalahev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name of the author may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
Mesa 3-D graphics library
Version: 7.0
Copyright (C) 1999-2007 Brian Paul All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
BRIAN PAUL BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copyright (c) 2007 The Khronos Group Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and/or associated documentation files (the
"Materials"), to deal in the Materials without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Materials, and to
permit persons to whom the Materials are furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Materials.
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
+251
View File
@@ -0,0 +1,251 @@
# GLEW - The OpenGL Extension Wrangler Library
The OpenGL Extension Wrangler Library (GLEW) is a cross-platform open-source C/C++ extension loading library. GLEW provides efficient run-time mechanisms for determining which OpenGL extensions are supported on the target platform. OpenGL core and extension functionality is exposed in a single header file. GLEW has been tested on a variety of operating systems, including Windows, Linux, Mac OS X, FreeBSD, Irix, and Solaris.
![](http://glew.sourceforge.net/glew.png)
http://glew.sourceforge.net/
https://github.com/nigels-com/glew
[![Build Status](https://travis-ci.org/nigels-com/glew.svg?branch=master)](https://travis-ci.org/nigels-com/glew)
[![Gitter](https://badges.gitter.im/nigels-com/glew.svg)](https://gitter.im/nigels-com/glew?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Download](https://img.shields.io/sourceforge/dm/glew.svg)](https://sourceforge.net/projects/glew/files/latest/download)
## Table of Contents
* [Downloads](#downloads)
* [Recent snapshots](#recent-snapshots)
* [Build](#build)
* [Linux and Mac](#linux-and-mac)
* [Using GNU Make](#using-gnu-make)
* [Install build tools](#install-build-tools)
* [Build](#build-1)
* [Linux EGL](#linux-egl)
* [Linux mingw-w64](#linux-mingw-w64)
* [Using cmake](#using-cmake)
* [Install build tools](#install-build-tools-1)
* [Build](#build-2)
* [Windows](#windows)
* [Visual Studio](#visual-studio)
* [MSYS/Mingw](#msysmingw)
* [MSYS2/Mingw-w64](#msys2mingw-w64)
* [glewinfo](#glewinfo)
* [Code Generation](#code-generation)
* [Authors](#authors)
* [Contributions](#contributions)
* [Copyright and Licensing](#copyright-and-licensing)
## Downloads
Current release is [2.1.0](https://sourceforge.net/projects/glew/files/glew/2.1.0/).
[(Change Log)](http://glew.sourceforge.net/log.html)
Sources available as
[ZIP](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.zip/download) or
[TGZ](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.tgz/download).
Windows binaries for [32-bit and 64-bit](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0-win32.zip/download).
### Recent snapshots
Snapshots may contain new features, bug-fixes or new OpenGL extensions ahead of tested, official releases.
[glew-20200115.tgz](https://sourceforge.net/projects/glew/files/glew/snapshots/glew-20200115.tgz/download) *GLEW 2.2.0 RC3: fixes*
[glew-20190928.tgz](https://sourceforge.net/projects/glew/files/glew/snapshots/glew-20190928.tgz/download) *GLEW 2.2.0 RC2: New extensions, bug fixes*
## Build
It is highly recommended to build from a tgz or zip release snapshot.
The code generation workflow is a complex brew of gnu make, perl and python, that works best on Linux or Mac.
The code generation is known to work on Windows using [MSYS2](https://www.msys2.org/).
For most end-users of GLEW the official releases are the best choice, with first class support.
### Linux and Mac
#### Using GNU Make
GNU make is the primary build system for GLEW, historically.
It includes targets for building the sources and headers, for maintenance purposes.
##### Install build tools
Debian/Ubuntu/Mint: `$ sudo apt-get install build-essential libxmu-dev libxi-dev libgl-dev`
RedHat/CentOS/Fedora: `$ sudo yum install libXmu-devel libXi-devel libGL-devel`
FreeBSD: `# pkg install xorg lang/gcc git cmake gmake bash python perl5`
##### Build
$ make
$ sudo make install
$ make clean
Targets: `all, glew.lib (sub-targets: glew.lib.shared, glew.lib.static), glew.bin, clean, install, uninstall`
Variables: `SYSTEM=linux-clang, GLEW_DEST=/usr/local, STRIP=`
_Note: you may need to call `make` in the **auto** folder first_
##### Linux EGL
$ sudo apt install libegl1-mesa-dev
$ make SYSTEM=linux-egl
##### Linux mingw-w64
$ sudo apt install mingw-w64
$ make SYSTEM=linux-mingw32
$ make SYSTEM=linux-mingw64
#### Using cmake
The cmake build is mostly contributer maintained.
Due to the multitude of use cases this is maintained on a _best effort_ basis.
Pull requests are welcome.
*CMake 2.8.12 or higher is required.*
##### Install build tools
Debian/Ubuntu/Mint: `$ sudo apt-get install build-essential libxmu-dev libxi-dev libgl-dev cmake git`
RedHat/CentOS/Fedora: `$ sudo yum install libXmu-devel libXi-devel libGL-devel cmake git`
##### Build
$ cd build
$ cmake ./cmake
$ make -j4
| Target | Description |
| ---------- | ----------- |
| glew | Build the glew shared library. |
| glew_s | Build the glew static library. |
| glewinfo | Build the `glewinfo` executable (requires `BUILD_UTILS` to be `ON`). |
| visualinfo | Build the `visualinfo` executable (requires `BUILD_UTILS` to be `ON`). |
| install | Install all enabled targets into `CMAKE_INSTALL_PREFIX`. |
| clean | Clean up build artifacts. |
| all | Build all enabled targets (default target). |
| Variables | Description |
| --------------- | ----------- |
| BUILD_UTILS | Build the `glewinfo` and `visualinfo` executables. |
| GLEW_REGAL | Build in Regal mode. |
| BUILD_FRAMEWORK | Build as MacOSX Framework. Setting `CMAKE_INSTALL_PREFIX` to `/Library/Frameworks` is recommended. |
### Windows
#### Visual Studio
Use the provided Visual Studio project file in build/vc15/
Projects for vc6, vc10, vc12 and vc14 are also provided
#### MSYS/Mingw
Available from [Mingw](http://www.mingw.org/)
Requirements: bash, make, gcc
$ mingw32-make
$ mingw32-make install
$ mingw32-make install.all
Alternative toolchain: `SYSTEM=mingw-win32`
#### MSYS2/Mingw-w64
Available from [Msys2](http://msys2.github.io/) and/or [Mingw-w64](http://mingw-w64.org/)
Requirements: bash, make, gcc
$ pacman -S gcc make mingw-w64-i686-gcc mingw-w64-x86_64-gcc
$ make
$ make install
$ make install.all
Alternative toolchain: `SYSTEM=msys, SYSTEM=msys-win32, SYSTEM=msys-win64`
## glewinfo
`glewinfo` is a command-line tool useful for inspecting the capabilities of an
OpenGL implementation and GLEW support for that. Please include `glewinfo.txt`
with bug reports, as appropriate.
---------------------------
GLEW Extension Info
---------------------------
GLEW version 2.0.0
Reporting capabilities of pixelformat 3
Running on a Intel(R) HD Graphics 3000 from Intel
OpenGL version 3.1.0 - Build 9.17.10.4229 is supported
GL_VERSION_1_1: OK
---------------
GL_VERSION_1_2: OK
---------------
glCopyTexSubImage3D: OK
glDrawRangeElements: OK
glTexImage3D: OK
glTexSubImage3D: OK
...
## Code Generation
A Unix or Mac environment is needed for building GLEW from scratch to
include new extensions, or customize the code generation. The extension
data is regenerated from the top level source directory with:
make extensions
An alternative to generating the GLEW sources from scratch is to
download a pre-generated (unsupported) snapshot:
https://sourceforge.net/projects/glew/files/glew/snapshots/
## Authors
GLEW is currently maintained by [Nigel Stewart](https://github.com/nigels-com)
with bug fixes, new OpenGL extension support and new releases.
GLEW was developed by [Milan Ikits](http://www.cs.utah.edu/~ikits/)
and [Marcelo Magallon](http://wwwvis.informatik.uni-stuttgart.de/~magallon/).
Aaron Lefohn, Joe Kniss, and Chris Wyman were the first users and also
assisted with the design and debugging process.
The acronym GLEW originates from Aaron Lefohn.
Pasi K&auml;rkk&auml;inen identified and fixed several problems with
GLX and SDL. Nate Robins created the `wglinfo` utility, to
which modifications were made by Michael Wimmer.
## Contributions
GLEW welcomes community contributions. Typically these are co-ordinated
via [Issues](https://github.com/nigels-com/glew/issues) or
[Pull Requests](https://github.com/nigels-com/glew/pulls) in the
GitHub web interface.
Be sure to mention platform and compiler toolchain details when filing
a bug report. The output of `glewinfo` can be quite useful for discussion
also.
Generally GLEW is usually released once a year, around the time of the Siggraph
computer graphics conference. If you're not using the current release
version of GLEW, be sure to check if the issue or bug is fixed there.
## Copyright and Licensing
GLEW is originally derived from the EXTGL project by Lev Povalahev.
The source code is licensed under the
[Modified BSD License](http://glew.sourceforge.net/glew.txt), the
[Mesa 3-D License](http://glew.sourceforge.net/mesa.txt) (MIT) and the
[Khronos License](http://glew.sourceforge.net/khronos.txt) (MIT).
The automatic code generation scripts are released under the
[GNU GPL](http://glew.sourceforge.net/gpl.txt).
+1
View File
@@ -0,0 +1 @@
2.2.0
+3051
View File
File diff suppressed because it is too large Load Diff
+26427
View File
File diff suppressed because it is too large Load Diff
+1831
View File
File diff suppressed because it is too large Load Diff
+1468
View File
File diff suppressed because it is too large Load Diff
+31949
View File
File diff suppressed because it is too large Load Diff
@@ -1,28 +0,0 @@
diff --git a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
index 6f63781..9b1c08e 100644
--- a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
+++ b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
@@ -210,10 +210,10 @@ private:
// Define two pcurves of the seam-edge.
occ::handle<Geom2d_Curve> aPC1, aPC2;
- double af, al;
+ double af, al, af1, al1;
aE.Orientation(TopAbs_FORWARD);
- aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
+ aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af1, al1);
aE.Orientation(TopAbs_REVERSED);
aPC2 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
@@ -224,7 +224,9 @@ private:
}
// Select the correct pcurve of the seam-edge.
- const gp_Pnt2d& aFPntOfPC1 = aPC1->Value(aPC1->FirstParameter());
+ // Use the edge's first parameter. A Geom2d_Line's FirstParameter() is -Precision::Infinite(),
+ // where a direction of (2e-16, -1) from rounding error gives an X far outside the U range.
+ const gp_Pnt2d aFPntOfPC1 = aPC1->Value(af1);
if (std::abs(aLPntOfIPC1.X() - aFPntOfPC1.X()) > Precision::Confusion())
{
-7
View File
@@ -25,16 +25,9 @@ endif()
# shipped bytes. Windows ships only the DLLs libslic3r links, so the tab adds the TKFillet,
# TKOffset and TKBool DLLs. See docs/HLSD/design-tab.md.
if (IN_GIT_REPO)
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
endif ()
orcaslicer_add_cmake_project(OCCT
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V8_0_1.zip
URL_HASH SHA256=7c033d917ee8f040c0512d289dcc5f02c148889d5bac17c3e25639accb44f0da
# Makes BRepMesh triangulate cone faces whose seam pcurve is slightly tilted
# (Open-Cascade-SAS/OCCT#572); remove the patch once an OCCT release includes the fix.
PATCH_COMMAND git apply ${OCCT_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
#DEPENDS dep_Boost
DEPENDS ${FREETYPE_PKG}
CMAKE_ARGS
+101
View File
@@ -0,0 +1,101 @@
cmake_minimum_required(VERSION 3.0)
project(OpenCSG)
if (NOT BUILD_SHARED_LIBS)
set(GLEW_USE_STATIC_LIBS ON)
elseif (MSVC)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
find_package(OpenGL REQUIRED)
set(GLEW_VERBOSE ON)
find_package(GLEW 1.13.0 REQUIRED)
set(_srcfiles
src/area.cpp
src/batch.cpp
src/context.cpp
src/channelManager.cpp
src/frameBufferObject.cpp
src/frameBufferObjectExt.cpp
src/occlusionQuery.cpp
src/opencsgRender.cpp
src/openglHelper.cpp
src/pBufferTexture.cpp
src/primitive.cpp
src/primitiveHelper.cpp
src/renderGoldfeather.cpp
src/renderSCS.cpp
src/scissorMemo.cpp
src/settings.cpp
src/stencilManager.cpp
RenderTexture/RenderTexture.cpp
include/opencsg.h
src/opencsgConfig.h
src/area.h
src/batch.h
src/context.h
src/channelManager.h
src/frameBufferObject.h
src/frameBufferObjectExt.h
src/occlusionQuery.h
src/offscreenBuffer.h
src/opencsgRender.h
src/openglHelper.h
src/pBufferTexture.h
src/primitiveHelper.h
src/scissorMemo.h
src/settings.h
src/stencilManager.h
)
add_library(opencsg ${_srcfiles})
target_include_directories(opencsg PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
target_include_directories(opencsg PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>)
target_link_libraries(opencsg PRIVATE GLEW::GLEW OpenGL::GL)
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
VERSION 1.4.2
COMPATIBILITY AnyNewerVersion
)
install(TARGETS opencsg
EXPORT ${PROJECT_NAME}Targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
export(EXPORT ${PROJECT_NAME}Targets
FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
NAMESPACE ${PROJECT_NAME}:: )
set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(EXPORT ${PROJECT_NAME}Targets
FILE
"${PROJECT_NAME}Config.cmake"
NAMESPACE
${PROJECT_NAME}::
DESTINATION
${ConfigPackageLocation}
)
install(
FILES
${PROJECT_SOURCE_DIR}/include/opencsg.h
DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}/opencsg
)
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION
${ConfigPackageLocation}
)
+17
View File
@@ -0,0 +1,17 @@
orcaslicer_add_cmake_project(OpenCSG
# GIT_REPOSITORY https://github.com/floriankirsch/OpenCSG.git
# GIT_TAG 83e274457b46c9ad11a4ee599203250b1618f3b9 #v1.4.2
URL https://github.com/floriankirsch/OpenCSG/archive/refs/tags/opencsg-1-4-2-release.zip
URL_HASH SHA256=51afe0db79af8386e2027d56d685177135581e0ee82ade9d7f2caff8deab5ec5
PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt
DEPENDS dep_GLEW
)
if (TARGET ${ZLIB_PKG})
add_dependencies(dep_OpenCSG ${ZLIB_PKG})
endif()
if (MSVC)
add_debug_dep(dep_OpenCSG)
endif ()
-29
View File
@@ -44,18 +44,6 @@ else()
if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
else()
# A static library that is embedded into a shared object must not export
# its symbols. On Linux the running process also loads the system OpenSSL
# 3.x (WebKitGTK/gnutls pull in libcrypto.so.3), and CPython's _ssl and
# _hashlib are dlopened (RTLD_LOCAL) DSOs that each embed this OpenSSL.
# With default visibility their unversioned OpenSSL references are
# preempted by that global 3.x copy, mixing the 1.1.1 and 3.x ABIs and
# corrupting the heap (ssl.create_default_context() aborts). Hidden
# visibility makes each embedded copy self-contained. Linux-only: macOS
# binds dylibs with a two-level namespace (no interposition) and ships no
# OpenSSL, and Windows has no equivalent flag and no system OpenSSL to
# collide with.
set(_openssl_extra_cflags -fvisibility=hidden)
set(_conf_cmd env "CC=${CMAKE_C_COMPILER}" "LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}" "./config")
endif()
set(_cross_comp_prefix_line "")
@@ -114,20 +102,3 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
COMMAND ${CMAKE_COMMAND} -E copy_directory openssl "${DESTDIR}${CMAKE_INSTALL_LIBDIR}/cmake/openssl"
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
)
if (NOT WIN32 AND NOT APPLE)
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
# example to add -fvisibility=hidden) relinks the archives from stale
# objects instead of recompiling them, and the change silently has no
# effect. Drop the objects whenever this recipe changes so the next build
# actually recompiles them.
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
ExternalProject_Add_Step(dep_OpenSSL clean_objects
DEPENDEES configure
DEPENDERS build
COMMAND make clean
WORKING_DIRECTORY "${SOURCE_DIR}"
DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
COMMENT "OpenSSL: cleaning objects after a recipe change"
)
endif ()
-24
View File
@@ -299,27 +299,3 @@ endif()
if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB)
endif()
if (NOT WIN32 AND NOT APPLE)
# CPython's Makefile rules for _ssl and _hashlib depend only on their own
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
# them relink and they keep the previous symbols. On an incremental tree,
# drop the built modules and relink them against the current OpenSSL; a
# fresh build is left alone (its PGO target builds them). "make" alone is a
# no-op once PGO has run, so sharedmods is invoked explicitly.
ExternalProject_Get_Property(dep_python3 SOURCE_DIR)
file(GLOB _python_ssl_modules
"${SOURCE_DIR}/Modules/_ssl*.so"
"${SOURCE_DIR}/Modules/_hashlib*.so")
if (_python_ssl_modules)
ExternalProject_Add_Step(dep_python3 relink_ssl_extensions
DEPENDEES configure
DEPENDERS build
COMMAND sh -c "rm -f '${SOURCE_DIR}'/Modules/_ssl*.so '${SOURCE_DIR}'/Modules/_hashlib*.so && make -j${NPROC} sharedmods"
WORKING_DIRECTORY "${SOURCE_DIR}"
COMMENT "CPython: relinking _ssl/_hashlib against the current OpenSSL"
DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
"${CMAKE_CURRENT_LIST_DIR}/../OpenSSL/OpenSSL.cmake"
)
endif ()
endif ()
@@ -1111,24 +1111,7 @@ private:
default: ; // DONT_ALIGN
}
auto d = cb - ci;
// Keep the pile on the bin. Centring it on a target near an edge (a belt
// printer starts its parts at the leading end of the belt) would push a pile
// larger than the room around that point off the bed, so it stops at the
// edge instead; a pile that does not fit along an axis is centred on it.
// The items' boxes carry their inflation, which is the margin left at the edge.
auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
if (hi - lo >= bin_hi - bin_lo)
return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
if (lo + shift < bin_lo)
shift = bin_lo - lo;
if (hi + shift > bin_hi)
shift = bin_hi - hi;
return shift;
};
setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d)));
setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d)));
auto d = cb - ci;
// BBS make sure the item won't clash with excluded regions
// do we have wipe tower after arranging?
@@ -1157,7 +1140,9 @@ private:
return;
}
Item objs_convex_hull_item(objs_convex_hull);
Vertex ref_aligned = objs_convex_hull_item.referenceVertex() + d;
Vertex objs_convex_hull_ref = objs_convex_hull_item.referenceVertex();
Vertex diff = objs_convex_hull_ref - sl::boundingBox(objs_convex_hull).center();
Vertex ref_aligned = cb + diff; // reference point when pile center aligned with bed center
bool ref_aligned_is_ok = std::any_of(nfps.begin(), nfps.end(), [&ref_aligned](auto& nfp) {return sl::isInside(ref_aligned, nfp); });
if (!ref_aligned_is_ok) {
// ref_aligned is not good, then find a nearest point on nfp boundary
+1 -1
View File
@@ -191,7 +191,7 @@ public:
tail->next = std::move(p);
tail = new_tail;
}
disrupt_wait_for_data();
data_cond.notify_one();
}
void wait_and_pop(T& value)
-221
View File
@@ -1,221 +0,0 @@
# Adaptive TPMS infill — High Level Design
## Purpose and scope
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top
and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it.
The design has two parts: a field built once per object, and a pattern made
from it, warped around the center of each lobe of a body so that its cell size
follows the field, or, in the modes following the distance to the surface,
split into shells or blended between densities.
## Radial field
`TpmsRadialField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the
field depends on the geometry only, the densities are applied per region in the
fill. An object thinner than the grid cells has no body in the field; no field
is kept then, and the infill falls back to the regular pattern. In the modes
following the distance to the surface, the field also gives the depth of every
point (see below).
A regular 3D grid of cubic cells is rasterized from the `lslices` of the layers,
so the field follows what is printed: negative volumes, the union of
overlapping parts and holes are taken into account, and the mesh does not need
to be closed. A padding node around the grid is always outside. The grid is
capped at about a million nodes, with cells no smaller than 0.5 mm.
- Bodies are the connected inside nodes. Each is graded on its own, so separate
parts of one object each get their own sparse center.
- A body is split into lobes around the local maxima of the depth, by an exact
Euclidean distance transform (Felzenszwalb and Huttenlocher, one pass per
axis). Two maxima are in separate lobes when the depth along the segment
between them drops below 0.8 of the shallower one, like at the neck between
two united spheres; maxima shallower than 0.3 of the deepest one are ignored.
A maximum joins the first lobe whose first maximum it sees without a neck.
The lobes are made one at a time, the remaining maxima tested against the
first one in parallel, as a plate has a whole plane of them.
Where the depth ties along a line or a plane, as in a tall box, the lobe's
center is the node nearest to the middle of the tied nodes, so the center is
in the middle of the height and not a column.
- A point belongs to the lobe it is nearest to relative to their depths, so the
side between two lobes is nearer to the smaller one. Near that side, within a
tenth of that relative distance, the patterns of the lobes morph into each
other, so the lines stay continuous. Every lobe in that range takes part, up
to four, so the morph is also continuous where three or four lobes meet.
- The reach of a lobe is the distance from its center to the first exit along
24 x 48 latitude-longitude directions, smoothed twice over neighbouring
directions in log space. Towards a neighbouring lobe it stops at twice the
distance to the side between them, so that side is graded half way, as deep
as a neck is, rather than as sparse as the center or as dense as the surface.
Only the lobes whose centers are near enough to be nearer at the current
distance are compared along a ray, so many lobes, as in a perforated plate,
stay cheap.
The radial coordinate of a point is its distance to the center over the reach
in its direction. Behind a gap, as across the hole
of a ring, the radial coordinate is above 1 and the infill keeps the surface
density.
- Every outside node belongs to its nearest body, so points near a surface find
their body without a search. With a single body, all nodes belong to it.
In the 2D modes, every plane of nodes normal to the axis is a field of its own:
the distance transform skips the axis, bodies, lobes and the nearest body are
found within the plane, and the reach is sampled on a circle of 48 directions.
A point is looked up in the two planes around it, the weights of their lobes
interpolated along the axis, so the grading does not step between planes; a
plane without a body uses the nearest one that has one. The planes are a cell
apart, not a layer: where the sections change abruptly, as at a step, the
patterns of the two planes morph into each other over that cell.
A distance to the nearest surface would be the obvious field, but no smooth map
follows it. By the divergence theorem, the mean scale of a map over a body is
fixed by its values on the surface: a map that keeps the full density along the
whole surface, as the distance would ask under the top and bottom, has the mean
density of the uniform infill, the sparser core being paid for by lines crowding
along the walls. The layers of a plate at different depths would also need
different line spacings in the same directions, which no continuous map allows
without shearing across the plate. Following the distance needs changes of the
topology of the lattice (see below). The radial coordinate instead grades what a
single map can: towards one point.
## Warped pattern
The pattern is evaluated on warped coordinates:
TPMS(f_surface * m(t) * (p - center))
where `m` scales the pattern around the center of the lobe: its frequency is
`m + t * m'` along the ray and `m` across it. `m(t)` is the mean of the target
scale over the ball of radius `t`, `3 / t^3 * integral of s^2 * target(s) ds`, so
the mean of the three, and with it the density, follows the gradient. The cells
are round at the center; near the surface they are flattened, with the lines
running parallel to it. Beyond the surface the target is the surface scale, so
the warp extends continuously outside.
In the 2D modes only the coordinates within the plane are warped, and `m(t)` is
the mean over the disc, `2 / t^2 * integral of s * target(s) ds`. Along the axis
the pattern keeps the interior frequency: scaling it with `m` would shear the
pattern by the distance along the axis times the gradient of `m`, without bound
on a long object. The cells are round at the center and stretched along the
axis near the surface. With Normal Z the layers are graded exactly, since
the lines of a layer follow its in-plane frequencies; normal to X or Y, the
layers near the sides are as dense as the larger of the two frequencies in the
layer, which is the surface one.
Evaluating a TPMS at a frequency that varies with the position without such a
map distorts it wherever the frequency changes, because the phase also changes
with the gradient of the frequency times the distance from the origin. Fitting a
smooth map to a varying isotropic scale in the least-squares sense (a Poisson
problem per axis) cannot grade strongly: its divergence is the target scale plus
a harmonic function pinned by the surface, which keeps the scale in the core
near two thirds of the surface one. Following a distance exactly needs the
lattice to change its topology, by blending lattices of different densities or
filling shells of equal distance with them, as the modes following the distance
to the surface do.
The target scale at depth `d = 1 - t`, with `S` the surface and `I` the interior
frequency, both from each pattern's own density calibration:
| Gradient | Scale |
|-------------|------------------------|
| Linear | `1 + (I / S - 1) * d` |
| Quadratic | `1 + (I / S - 1) * d^2`|
| Exponential | `(I / S)^d` |
With a denser surface, quadratic keeps the surface density deepest and
exponential drops fastest. A denser interior works the same way.
The zero level is extracted with marching squares like the regular TPMS-FK, on
a sampling grid fixed in the fill frame like the optimized Gyroid, so that every
region of a layer connects its lines the same way at the saddles of the pattern.
Loops narrower than two lines (shorter than `2 * PI * spacing`) are dropped, as
they would print as blobs. The fill works in a frame rotated by the infill
angle, so the radial field is looked up at the point rotated back into the
object frame, and the center rotated into the fill frame. Both use the middle of
the layer.
## Modes following the distance to the surface
`distance_warp`, `smooth_blend` and `stepped_shells` grade by the distance to
the nearest surface, including the top and bottom, as concentric shells do. The
depth of a point is that distance over the distance of the deepest point of its
body, from 0 at the surface to 1; the field keeps it for every node and
interpolates it between them. A tall box so keeps its whole axis as sparse as
its center, and a plate is graded through its thickness.
No single smooth pattern follows that depth without distortion (see above), so
the three modes trade differently:
- Distance warp keeps the lobes and the warp of Lobes, but its radial profile
comes from the depth. For every direction of a lobe, the mean depth over the
ball along the ray is sampled at 33 radii up to the reach, then smoothed over
the neighbouring directions like the reach. The radial coordinate is the one
of the linear profile with the same mean depth, `t = 4/3 * (1 - mean depth)`,
so with a linear gradient the mean cell size follows the depth exactly, and
with the others approximately. In a sphere or a cube, where the depth falls
linearly along every ray, it is Lobes. Elsewhere the profile changes with the
direction, and the warp shears where neighbouring directions differ, as in
plates and long bodies; right under the top of a long body the cells are
sparser within the layer, the warp moving their density into the height.
The profiles are smoothed over the directions like the reach, as sharper
ones shear the pattern across the layer, which adds lines. A long body is so
graded partly along its length, between Lobes and the distance.
- Smooth blend evaluates the regular patterns of the two levels around the
target of every point and blends them by a smoothstep over the whole gap
between the levels, here at most 2.5 times apart. The density follows the
depth without steps, but where two lattices blend, part of their lines run
along the blend, so fewer levels print fewer extra lines. From 25% to 5%,
three levels print about 0.45 of the uniform infill in a deep core whose
levels alone would print 0.3; levels 1.5 times apart print 0.6 to 0.7, and a
single blend from the surface to the interior 0.6.
- Stepped shells split each region of a layer into shells and fill every shell
with the regular pattern at its density. The densities are levels from the
surface to the interior density at most 1.5 times apart, five from 25% to 5%,
and a point takes the level nearest to its target on that geometric scale.
The shells are traced by marching squares of the continuous level over the
layer on a 0.5 mm grid fixed in the object, so every region of a layer gets
the same shells, and clipped to the region. Each shell is shrunk by half a
line, like a filled region, and its regular filler connects its lines along
that boundary, so the connections of two neighbouring shells lie side by side
instead of on top of each other. The pattern is never
distorted, but its lines end at every shell, and thin parts get thin shells.
The connections add lines: in the core of a 60 mm cube, about a third more
than the target.
Stepped shells and Smooth blend need neither lobes nor reaches, which are not
built for them.
## Constraints
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large.
- At a sparse infill density of 100% the sparse infill is turned into solid
infill, so there is nothing to grade: the options are hidden and no field is
built.
- The adaptive options invalidate `posPrepareInfill`, which rebuilds the field
and the anchoring infill.
- An elongated body without a neck has one center, so its far ends are graded
as the outer part of the body, and the warp shears where the reach changes
quickly with the direction. A concave body, like an L, may be split into lobes
where its maxima cannot see each other in a straight line.
- Across a ray, the scale is the mean of the gradient from the center, so the
layers right under the top and above the bottom are sparser than the surface
density in their middle, and a plate is graded from its middle outwards rather
than through its thickness.
-172
View File
@@ -1,172 +0,0 @@
# Center of mass markers — High Level Design
## Purpose and scope
The "Center of mass" item of the canvas toolbar menu, in the bottom left corner of the 3D view, marks
where the mass of the plate, of each object instance and of each body of an assembly is centered,
in Prepare and in Preview. It helps judge how parts will rest on the plate, for instance whether a
tall or leaning part could tip. 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
`show_center_of_mass`, off by default. The assembly view and the Design tab have no markers.
Three kinds of marker share one shape, a sphere whose octants alternate between two colors:
- each plate, black and white, for everything on it;
- each object instance, light blue and white;
- each body of an assembly, red and yellow;
- in Preview, the supports and raft of each object instance, green and black.
A click on a marker opens a box beside it with the weight and volume of what it stands for, where its
center lies in that thing's bounding box and the size of the box, and its moments of inertia about
axes through the center parallel to x, y and z.
An assembly is an object of several parts or with negative volumes. Its bodies are the connected
solids its parts make once united, the bodies the separated infills option centers its infill on
(see separated-infills.md): parts that overlap or touch are one body, parts apart are separate
bodies. An object of one body, and every object of a single part, has no body markers, as its object
marker says it all.
Mass is volume times density, the `filament_density` of the filament that prints it, or 1.245 g/cm³
(`DEFAULT_FILAMENT_DENSITY`) for a filament without one. Prepare has the model only, so it takes each
part as a solid of the density of its filament, the part's own or else its object's. It reads each
filament's density from the filament's selected preset, edits not yet saved included, as slicing does:
the plater's own config holds the values of the filament edited last only. Preview has
what will be printed, so its markers come from the toolpaths, whose mass depends on walls, infill
and flow as well. There the solid markers are for what is printed up to the top layer the layer
slider shows: for the plate with brim, raft and supports, where the weight rests at that point of
the print; for an object or a body, its own extrusions. Each has a faded twin for the same at the
end of the print, so the slider shows the weight moving toward where it ends, and at the top layer
the two meet. In Prepare the plate has the model alone, as brim, raft and supports exist only once
sliced.
## Prepare: from the meshes
An object of one part takes the mass properties of its mesh at unit density, times its density, from
`its_mass_properties()`, which handles a mesh in a single pass. Each triangle and a fixed vertex of
the mesh span a tetrahedron whose signed volume is `V = a · (b × c) / 6`, with `a`, `b`, `c` taken
relative to that vertex. By the divergence theorem these volumes add up to the volume of a closed
mesh, their volume-weighted centroids to its center of mass, and their second moments
`V (a aᵀ + b bᵀ + c cᵀ + s sᵀ) / 20`, with `s = a + b + c`, to its own. Shells facing inward, such as
a cavity, subtract themselves, and flipping every triangle changes nothing. The sums are kept in
double precision and relative to a vertex of the mesh rather than the origin, which keeps them exact
for meshes far from it. The result keeps the spread of the mass about its center,
`(x - c)(x - c)ᵀ` averaged over the mass, from which the moments of inertia follow.
The CGAL routines that look alike do not compute this. `CGAL::centroid` weighs tetrahedra by their
unsigned volume, so it fails on cavities and on any shell that is not star-shaped from the fan's
apex; over triangles it returns the centroid of the surface, and over points the average of the
vertices, which depends on the tessellation. `CGAL::barycenter` with the signed volumes as weights
gives the same answer, but only after copying every tetrahedron into a vector of weighted points,
and takes two and a half times as long.
A mesh's result is in its own coordinates. Each `GLVolume` maps it to the world with its world
matrix `M`, and weighs it by the volume times the absolute determinant of that matrix: a center of
mass moves with any affine map, and the spread becomes `L S Lᵀ` for the linear part `L` of `M`, so no
mesh is ever transformed. The `GLVolume`'s matrices, rather than the
model's, let the markers follow an object while it is dragged, before the model is updated. Results
are cached by `ModelVolume` id; a `ModelVolume` takes a new id whenever its mesh changes, which is
the rule `reload_scene()` relies on to rebuild a `GLVolume`'s geometry, so a cached result never
outlives its mesh.
An assembly's parts overlap or touch, which the mesh formula would count twice, so `solid_bodies()`
slices them instead, in the object's coordinates. It cuts the height into 500 slabs, 100 while a part
is dragged, slices every part and negative volume at the middle of each slab, unites the parts and
cuts the negative volumes away, and links the islands of neighboring slabs that overlap into bodies
with `connected_bodies()`. Each island adds a prism of the slab's thickness at the density: its
area, and its first and second moments of area, from the same sums over the outline as the area,
with the slab's height for z. Where parts of different densities overlap, the later volume of the object counts, as
slicing clips every part by the parts after it; each part then weighs the region it prints, which is
credited to the island holding it. Each body also keeps the convex hull of its islands and the height
they span, whose corners, once transformed, give its bounding box, tight while the instance turns
about z only. The object is the sum of its bodies, its box that of its parts, as the object's size
shows it, and each plate the sum of the object instances `PartPlateList::find_instance()` puts on it,
so that an instance on no plate counts in none. The bodies are cached by `ModelObject` id with the volumes, types, densities and
transformations they were sliced from.
## Preview: from the toolpaths
`GCodeProcessor` sums the masses while it processes the G-code, in the same pass that builds the
moves, and leaves them in `GCodeProcessorResult`; `GCodeViewer` keeps a copy of them when it loads a
result. Nothing is stored per move.
Each extrusion weighs the volume of filament its E extrudes times the density of the filament that
extrudes it, so a print of several materials weighs each as it is. Flow ratio, line widths, ironing
and purging into infill all count
as printed. Its mass spreads evenly along the segment the bead's center runs, half the layer height
below the nozzle, in the frame of the stored moves: plate offset added, Z offset removed. Such a
segment from `a` to `b` adds `m (a + b) / 2` to the moments and `m (a² + a b + b²) / 3` to the second
moments along each axis, and its box widened by half the bead's height to the bounding box; not by
half its width, which the processor only estimates, so that a box runs along the walls' center lines. Arcs are already split into segments by the processor. Walls, infill, top and bottom surfaces, ironing
and gap fill make the parts. The brim and the support roles, the raft among them, count only in what
the plate prints. The skirt, the prime tower and custom G-code count nowhere.
The plate takes every extrusion, so it needs nothing more. The objects and bodies need the sliced
objects, which the G-code does not describe, so the G-code export hands the processor a locator
built from the `Print`; G-code opened from a file, or from a project sliced earlier, has no `Print`
behind it, and so shows the plate alone. Object labels would not do: profiles turn the four kinds
Orca writes on and off in every combination, and none of them tells the bodies apart.
The locator numbers the object instances and, for each assembly, the bodies of every instance. It
takes the bodies `PrintObject::prepare_infill()` found for separated infills, or, when that option
did not need them, links the islands (`Layer::lslices`) of neighboring layers into bodies with the
same `connected_bodies()`. For each extrusion of a part, it finds the first layer printed at or above
its height, as spiral vase rises through each layer, and the island holding it with an
`IslandLocator`, the one `solid_bodies()` credits its regions with: by the island's box, widened by
1 mm for walls reaching past it, with a polygon test only where boxes overlap, and the nearest
outline where none holds the point. The boxes of one layer's islands say nothing of the other
instances, so an instance whose widened box reaches another's, as copies placed side by side do,
tests the outlines alone, and outside them the nearest outline of all such instances wins. The island
gives both the instance and the body. The island found last is tried first, as extrusions mostly follow each other on one
island. Brim, raft and supports lie outside the islands. The brim counts in the plate only; a support
or raft extrusion goes to the instance whose footprint, the box of its widened islands, holds it, the
one whose center is nearest among several, or else the nearest footprint, as supports stand below and
around their object.
Each mass holds, for each layer id, the running total of what is printed up to that layer, the last
of which is the faded marker's, so the solid marker for any slider position is a single lookup. The layer ids are those
the moves carry, which are also the layers of libvgcode and of the slider; in a print by object they
follow the order of printing, so the solid markers show the objects printed so far as they are.
## Drawing
`smooth_sphere()` with a resolution divisible by four leaves every triangle within one octant, so it
splits into two models drawn with the `gouraud_light` shader in each kind's two colors. The radius is
9 pixels for the plate, 7 for the objects, 6 for the supports and 5 for the bodies, scaled like the canvas toolbar for the
display's DPI and kept constant on screen through the camera's inverse zoom. They are drawn in that
order, so that markers at one place show as rings. The faded markers are the same spheres at 40%
opacity, drawn before all the solid ones, which show over them where both meet.
The centers usually lie inside the objects, so the markers are drawn without the depth test and show
through the objects and anything in front of them. Back face culling keeps the far half of a sphere
from covering the near one. They are drawn after the ambient occlusion pass, which would otherwise
darken them as the surface behind them, and before FXAA, which smooths their edges.
The markers are part of the cached scene, so toggling them, or changing a filament's density while
they are shown, marks the scene dirty, and moving the layer slider redraws the scene with the solid
markers where they belong. In Prepare they are hidden while any gizmo other than Move, Rotate, Scale
and Lay on face is open, since the others work on the surface a marker would cover, and a hidden
object has no markers.
## Details
The markers drawn last are kept, and a left click is tested against them before it selects: each
center and a point a radius to its right are projected to the screen, and the click hits a marker
within that distance. The solid markers are tested before the faded ones and the smaller kinds
before the larger, the order in which they cover each other. A hit opens the details of that marker
and keeps the click from changing the selection; a click anywhere else closes them. The box is an
ImGui window beside the marker, redrawn with the overlay from the markers of the last scene, so it
follows a dragged object, and in Preview the layer slider. It closes when its marker is gone, or when
the number of markers of its kind changes, as then it may stand for something else.
Its title says what the marker stands for: the plate, an object, an assembly or a part, the body of
an assembly. The G-code export lists the object instances for the processor, marking assemblies, as
it hands it the locator.
Each marker carries its sums: mass, volume, first moments and the second moments about the origin
along each axis, `Σ m x²`, `Σ m y²` and `Σ m z²`, which add up from parts to objects to plates. The
moment of inertia about the axis through the center parallel to x is then
`m (σy² + σz²)`, with `σ² = Σ m x² / m - c²` along each axis, and likewise for y and z. Masses are
kept in mg, volume times density in g/cm³, and shown in g, volumes in cm³ and moments of inertia in
g·mm². In Preview the box tells the finished print from what is printed up to the layer shown, the
two weighing differently, and both are placed in the bounding box of everything the marker holds
by the end.
-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.
+22 -38
View File
@@ -82,7 +82,9 @@ there.
strong point is placed, no later strong modifier and no weak modifier is
processed for that perimeter.
- **Weak:** every weak modifier applies. They are applied from the lowest
priority to the highest, so the highest one overwrites overlapping zones.
priority to the highest, so the highest one overwrites overlapping zones. A
Blocked modifier that fully contains a perimeter is the exception: it is
skipped there (see [Full containment](#full-containment)).
A strong modifier without a usable segment, even one whose fragments were all
discarded, passes the turn to the next one.
@@ -230,23 +232,20 @@ begin and end positions on the source contour.
### Full containment
A modifier that covers the whole perimeter has no boundaries on it. The policy
follows seam painting, where a whole perimeter can be painted in either color:
follows seam painting, where painting a whole perimeter green is a meaningful
choice and forbidding the seam all round is not:
- **Seam Enforced** types the whole perimeter, like a perimeter painted green all
round, with subdivision applied as described under [Weak modifiers](#weak-modifiers).
- **Seam Neutral** types the whole perimeter Neutral, like an unmarked perimeter,
clearing painting and lower zones.
- **Seam Blocked** types the whole perimeter Blocked, like a perimeter painted
red all round, overriding painting and lower zones. The seam cannot avoid such
a perimeter: the seam placer compares Blocked candidates with each other like
Neutral ones, so the seam is placed as on an unmarked perimeter. If the
perimeter stays Blocked after all weak zones, it is reported in the user
warning (see [Diagnostics and warnings](#diagnostics-and-warnings)), so the
user can check the modifier.
- **Seam Center, Left and Right** are skipped with the full-containment warning:
there is no intersection to place the point on.
- **Seam Blocked** is skipped for the perimeter, with the full-containment
warning. The seam cannot avoid the whole perimeter, so the modifier does not
override anything below it: lower zones and painting stay in effect.
- **Seam Center, Left and Right** are skipped with the same warning: there is no
intersection to place the point on.
Enforced, Neutral and Blocked take part in the usual priority order (see
Enforced and Neutral take part in the usual priority order (see
[Weak modifiers](#weak-modifiers)).
The perimeter is fully contained when the united intervals cover every source
@@ -312,9 +311,9 @@ into every perimeter that has a strong seam.
`collect_weak_modifier_segments()` extracts the segments of every weak modifier
before the polygon is modified, so all positions refer to the same contour. Each
segment becomes a zone with a type and two boundaries, kept in application
order, lowest priority first. Full containment of a weak modifier becomes a
whole-perimeter zone at its place in that order: it has no boundaries and takes
part in no insertion or helper step below. The boundaries
order, lowest priority first. Full containment of an Enforced or Neutral
modifier becomes a whole-perimeter zone at its place in that order: it has no
boundaries and takes part in no insertion or helper step below. The boundaries
carry their positions on the source contour; these remain as provenance after
insertion and are not indices into the modified polygon.
@@ -385,10 +384,9 @@ G-code export issues it as one non-critical warning with the ID
`SlicingPreciseSeamWarning`. It is a single line, "Precise Seam: <causes>. Seam
placement may differ from expected.", because the export warnings dialog shows
only the first line of each warning. Repeated warning events replace the
notification instead of appending to it. Except for the "covers an entire
perimeter" and "had no effect" causes, the causes name the modifier types
involved, as the menu names them, in menu order and each type once, for example
"(Seam Left, Seam Enforced)".
notification instead of appending to it. Except for the "had no effect" cause,
the causes name the modifier types involved, as the menu names them, in menu
order and each type once, for example "(Seam Left, Seam Enforced)".
The causes are:
- **failed to process some intersections (types):** at least one fragment was
@@ -397,18 +395,8 @@ The causes are:
a Seam Center, Left or Right modifier had more than one segment on a
perimeter (see [Strong modifiers](#strong-modifiers)).
- **a perimeter is fully inside a modifier, the modifier was not applied to it
(types):** a Seam Center, Left or Right modifier was skipped for a
(types):** a Seam Center, Left, Right or Blocked modifier was skipped for a
perimeter (see [Full containment](#full-containment)).
- **Seam Blocked modifier "<name>" of "<object>" covers an entire perimeter:**
a Seam Blocked modifier covered a whole perimeter (see
[Full containment](#full-containment)). Only perimeters whose candidates are
all Blocked after all weak zones contribute to this warning, and only through
the zone in effect there: `apply_weak_modifiers_to_perimeter()` checks the
final types and flags the modifier of the highest whole-perimeter zone when it
is Blocked. A lower whole zone is overwritten by it and is not reported. Only
the first flagged modifier in print and volume order is named, followed by
"(N in total)" when there are several. The type name reuses the menu's
translation.
- **modifier "<name>" of "<object>" had no effect on the seam (it might not reach
the centerline of the printed perimeter):** a modifier was evaluated on at
least one perimeter and never gave a segment, full containment or a discarded
@@ -432,8 +420,6 @@ The log records the following diagnostic markers:
- `[PreciseSeamNoEffect]` for every modifier of the "had no effect" cause, with
the object and modifier names. Unlike the user warning, the log lists all of
them.
- `[PreciseSeamFullyBlocked]` for every modifier of the "covers an entire
perimeter" cause, listed the same way.
Failures and recoveries are counted separately. The first 10 of each per
`init()` call are logged in detail, in parallel processing order; if a limit is
@@ -577,14 +563,12 @@ last modifier would.
priorities, weak boundaries that coincide or share an edge, enforced
subdivision, whole-perimeter weak zones with painting and priorities, weak
zones over painting's oversampled candidates, the warning type masks, usage
tracking for the "had no effect" and whole-perimeter Blocked warnings, volume
sorting of strong and weak groups, restoration of strong points after
alignment, raft layer indexing and structured slices. End-to-end tests slice
a real object with Precise Seam
tracking for the "had no effect" warning, volume sorting of strong and weak
groups, restoration of strong points after alignment, raft layer indexing and
structured slices. End-to-end tests slice a real object with Precise Seam
volumes and check the outer wall starts in the exported G-code: every strong
mode under several seam positions and with a raft, Enforced and Blocked zones,
a perimeter blocked all round under every seam position, a modifier with a
hole, and the user warning.
a modifier with a hole, and the user warning.
- [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover
enforced-patch selection independent of the contour start, fully painted
contours, duplicate removal, and `Print::apply()` synchronization through type
-97
View File
@@ -1,97 +0,0 @@
# Separated infills — High Level Design
## Purpose and scope
An object's infill patterns are laid out from one reference point, the center
of the object. When an object groups several parts that do not touch, every
part cuts the same object-wide pattern at a different place, so equal parts get
different infill. `separated_infills` lays the infill of every connected body
out from the center of that body instead, as if the body were sliced on its own.
The option covers sparse infill, internal solid infill and bridges. Top and
bottom surfaces are left to `center_of_surface_pattern`, which centers the
Archimedean Chords and Octagram Spiral surface patterns. The option is off by
default; with it off, or for an object made of a single body, no fill changes.
Adaptive Cubic and Support Cubic do not depend on the option: they always fill
each body on its own (see Octree infill).
## Bodies
`PrintObject::prepare_infill()` groups the islands of every layer (`lslices`)
into 3D connected bodies with `connected_bodies()` before bridges are detected,
so bridge anchors and printed infill share one origin. Islands on adjacent layers belong to one body
when their slices overlap. Parts that touch or overlap form one body. Separate
parts, disconnected islands of one mesh, and interleaved parts that never touch,
such as chain links, each form their own. Every island stores the index of its
body in `Layer::lslices_separated_component_ids`, and
`PrintObject::separated_body_bboxes()` holds the bounding box of each body over
all its layers.
The pass runs when a region uses separated infills, per-model surface centering
or an octree infill pattern. It is skipped when the object has one model part
that cannot be split, since a single body already shares the object center.
## Centering a fill
`infill_body()` matches each fill region to the island it overlaps most, among
the islands whose bounding boxes overlap it, and the filler takes the bounding
box of that island's body instead of the object's. The box covers every layer
of the body, which is the box the body gets when sliced alone, so patterns that
depend on its extent as well as its center come out the same too. Bridge
anchoring (`Layer::generate_sparse_infill_polylines_for_anchoring()`) makes the
same choice, so the anchors match the printed infill.
The patterns follow the body's box in one of two ways:
- Rectilinear and its variants, Line, Grid, Triangles, Tri-hexagon, Cubic,
Quarter Cubic, Lateral Lattice, Lateral Honeycomb and the plane-path patterns
(Hilbert Curve, Archimedean Chords, Octagram Spiral) are laid out from the
box: they phase their lines through its center, and Hilbert Curve and the Zig
Zag links start from its corner. `Fill::extended_object_bounding_box()`
extends the box about its center, so it also serves a box that is not
centered on the origin.
- Honeycomb, 3D Honeycomb, Cross Hatch, Gyroid, TPMS-D and TPMS-FK are laid out
from the coordinate origin, which is the object center. They return true from
`Fill::aligned_to_origin()`, and `Fill::fill_surface()` moves each region so
that the box center lands on the origin, fills it, and moves the paths back.
With the default box the center is the origin, so nothing moves.
`is_separable_infill_pattern()` lists these patterns. The settings show the
option only when the sparse infill pattern is one of them.
## Octree infill
Adaptive Cubic and Support Cubic take their lines from an octree, laid out from
the center of the mesh it is built from and refined near its surfaces. An
octree of the whole object would lay every part out from the object's center
and refine it near the other parts, so these patterns
(`is_octree_infill_pattern()`) always fill each body on its own, and the
settings hide the option for them.
For an object of several bodies, `PrintObject::prepare_adaptive_infill_data()`
builds one octree per body (`FillAdaptive::Octrees`) from the triangles of that
body only, which is the octree the body gets when sliced alone. Each connected
component of the mesh goes to the body that most of a few sampled triangles lie
on. A sample is taken a layer height inside the solid, behind the triangle, and
looked up in the islands of the nearest layer. Each internal bridge surface goes
to the body of its island. The fill takes the octree of the region's body, from
the same `infill_body()`. The octree of the whole object is built only for an
object of a single body, or when some body received no triangles, which then
uses it.
The line spacing of an octree comes from the density, line width and multiline
count of a region, so a modifier or a part with its own density needs octrees of
its own. `adaptive_fill_line_spacing()` gives the spacing of each region, and
`FillAdaptive::RegionOctrees` holds one set of octrees per distinct spacing,
shared by the regions that have it. A set is built only for the bodies its
regions fill. The fill takes the set of its region, then the octree of its body.
## Patterns left out
Lightning grows its trees over the whole object, so moving a reference point
cannot center it on one body. Concentric and Spiral Inset follow the outline of
each region and need no centering.
Solid infill at full density spaces its lines over the extent of each region,
so it is already independent of the other bodies. Only bridges, which keep
their line spacing, and the plane-path solid patterns depend on the center.
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 -32
View File
@@ -72,8 +72,6 @@ src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.hpp
src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp
src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp
src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp
src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp
src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp
src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp
@@ -112,7 +110,6 @@ src/slic3r/GUI/Jobs/ArrangeJob.cpp
src/slic3r/GUI/Jobs/OrientJob.cpp
src/slic3r/GUI/Jobs/FillBedJob.cpp
src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
src/slic3r/GUI/Jobs/RotoptimizeJob.hpp
src/slic3r/GUI/Jobs/BindJob.cpp
src/slic3r/GUI/Jobs/PrintJob.cpp
src/slic3r/GUI/Jobs/SendJob.cpp
@@ -141,7 +138,6 @@ src/slic3r/GUI/DeviceErrorDialog.cpp
src/slic3r/GUI/ExtraRenderers.cpp
src/slic3r/GUI/EditGCodeDialog.cpp
src/slic3r/GUI/Field.cpp
src/slic3r/GUI/Field.hpp
src/slic3r/GUI/GCodeViewer.cpp
src/slic3r/GUI/GLCanvas3D.cpp
src/slic3r/GUI/Calibration.cpp
@@ -168,8 +164,6 @@ src/slic3r/GUI/OptionsGroup.cpp
src/slic3r/GUI/PrintOptionsDialog.cpp
src/slic3r/GUI/SafetyOptionsDialog.cpp
src/slic3r/GUI/ParamsPanel.cpp
src/slic3r/GUI/IMEXModesCtrl.cpp
src/slic3r/GUI/IMEXFilamentPickerPopover.cpp
src/slic3r/GUI/PartPlate.cpp
src/slic3r/GUI/Plater.cpp
src/slic3r/GUI/Preferences.cpp
@@ -217,14 +211,9 @@ src/libslic3r/GCode/SeamPlacer.cpp
src/libslic3r/ExtrusionEntity.cpp
src/libslic3r/Flow.cpp
src/libslic3r/Format/AMF.cpp
src/libslic3r/Format/3mf.cpp
src/libslic3r/Format/bbs_3mf.cpp
src/libslic3r/miniz_extension.cpp
src/libslic3r/IMEXHelpers.cpp
src/libslic3r/Preset.cpp
src/libslic3r/Print.cpp
src/libslic3r/BeltPurge.cpp
src/libslic3r/AppConfig.cpp
src/libslic3r/PrintBase.cpp
src/libslic3r/PrintConfig.cpp
src/libslic3r/Zipper.cpp
@@ -262,7 +251,6 @@ src/slic3r/Utils/MKS.cpp
src/slic3r/Utils/Moonraker.cpp
src/slic3r/Utils/OctoPrint.cpp
src/slic3r/Utils/Repetier.cpp
src/slic3r/Utils/ESP3D.cpp
src/slic3r/GUI/SendMultiMachinePage.cpp
src/slic3r/GUI/MultiMachinePage.cpp
src/slic3r/GUI/MultiMachineManagerPage.cpp
@@ -317,8 +305,6 @@ src/slic3r/GUI/AuxiliaryDataViewModel.cpp
src/slic3r/GUI/AuxiliaryDialog.cpp
src/slic3r/GUI/BBLStatusBarPrint.cpp
src/slic3r/GUI/BBLStatusBarSend.cpp
src/slic3r/GUI/BBLStatusBar.cpp
src/slic3r/GUI/BeltPurgeTower.cpp
src/slic3r/GUI/BaseTransparentDPIFrame.cpp
src/slic3r/GUI/CloneDialog.cpp
src/slic3r/GUI/ColorDecomposeDialog.cpp
@@ -345,8 +331,6 @@ src/slic3r/GUI/OAuthDialog.cpp
src/slic3r/GUI/PluginPickerDialog.cpp
src/slic3r/GUI/PluginsConfigDialog.cpp
src/slic3r/GUI/PluginsDialog.cpp
src/slic3r/GUI/PrinterWebView.cpp
src/slic3r/GUI/Project.cpp
src/slic3r/GUI/PublishSettingsDialog.hpp
src/slic3r/GUI/PurgeModeDialog.cpp
src/slic3r/GUI/TerminalDialog.cpp
@@ -355,31 +339,16 @@ src/slic3r/GUI/TextureLibrary.cpp
src/slic3r/GUI/TextureProjectorFrame.cpp
src/slic3r/GUI/UVEditorCanvas.cpp
src/slic3r/GUI/UserManager.cpp
src/slic3r/GUI/WebDownPluginDlg.cpp
src/slic3r/GUI/WebViewDialog.cpp
src/slic3r/GUI/Widgets/AMSItem.cpp
src/slic3r/GUI/Widgets/DialogButtons.cpp
src/slic3r/Utils/NetworkAgentFactory.cpp
src/slic3r/Utils/MoonrakerPrinterAgent.cpp
src/slic3r/Utils/BBLNetworkPlugin.cpp
src/slic3r/Utils/OrcaCloudServiceAgent.cpp
src/slic3r/plugin/PluginAuditManager.cpp
src/slic3r/plugin/PluginConfig.cpp
src/slic3r/plugin/PluginResolver.cpp
src/slic3r/plugin/CloudPluginService.cpp
src/slic3r/plugin/PluginFsUtils.cpp
src/slic3r/plugin/PluginHooks.cpp
src/slic3r/plugin/PluginLoader.cpp
src/slic3r/plugin/PluginManager.cpp
src/slic3r/plugin/PythonPluginBridge.cpp
src/libslic3r/CAD/SketchEngine.cpp
src/libslic3r/CAD/GeometryEngine.cpp
src/libslic3r/FilamentMixer.cpp
src/libslic3r/Config.cpp
src/libslic3r/Format/STEP.cpp
src/libslic3r/Format/svg.cpp
src/libslic3r/Extruder.cpp
src/libslic3r/MeshBoolean.cpp
src/libslic3r/utils.cpp
src/slic3r/GUI/CAD/DesignOffer.hpp
src/slic3r/GUI/CAD/DesignSketchTool.cpp
src/slic3r/GUI/EncodedFilament.cpp
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,79 +0,0 @@
#!/usr/bin/env python3
"""Belt temperature-tower asset generator (discrete-provini design).
A vertical temperature tower cannot be sliced on a belt printer, so lay a row of
DISCRETE provini (one per temperature) along the belt (designed Y) with a fixed
surface gap. Each provino is the chevron+arc unit (belt_temp_provino_unit.stl,
keel-first); its temperature is ENGRAVED upright into the 50 mm face — a raised
number would be an unsupported overhang on the belt. The C++ calib_temp belt branch
(Plater.cpp) injects one M104 per zone 70 layers INTO provino i:
print_z[i] = i * PITCH * cos(theta) + 70 * layer_height (theta = 45)
inside the body, not in the empty inter-provino gap (which has no sliced layers for
the event to attach to). PITCH below is the shared geometry contract with that code —
keep them in sync.
Generates one STL per filament temp range used by Temp_Calibration_Dlg.
"""
import numpy as np, trimesh, os
from matplotlib.textpath import TextPath
from matplotlib.font_manager import FontProperties
from shapely.geometry import Polygon as ShPoly
from shapely.ops import unary_union
HERE = os.path.dirname(os.path.abspath(__file__))
UNIT = os.path.join(HERE, 'belt_temp_provino_unit.stl') # single provino, keel-first
SURF_GAP = 25.0 # surface-to-surface gap between provini (mm) — user spec
TEXT_H = 9.0
TEXT_DEPTH = 0.8 # engraving depth (numbers are CUT into the face, not raised:
# a raised number is an unsupported Y-overhang on the belt)
TEXT_OVERSHOOT = 0.6 # extra height poking out of the face for a clean boolean cut
# Temperature ranges (start, end) per filament family, 5 C step. File name encodes them.
RANGES = [(230,190),(270,230),(250,230),(280,240),(240,210),(320,280)]
unit = trimesh.load(UNIT)
dY = unit.bounds[1,1] - unit.bounds[0,1]
PITCH = dY + SURF_GAP # designed-Y pitch == C++ contract constant
print(f"unit dY={dY:.2f} PITCH={PITCH:.3f} (C++ contract: print_z[i]=i*{PITCH:.3f}*cos45)")
# 50 mm face normal (0,-1,1)/sqrt2 ; UPRIGHT basis u=+X det(+1) (verified non-mirrored)
n = np.array([0,-1,1.])/np.sqrt(2)
u = np.array([1,0,0.]); v = np.array([0,1,1.])/np.sqrt(2)
R = np.column_stack([u,v,n])
fn = unit.face_normals; fc = unit.triangles_center; fa = unit.area_faces
sel = (fn@n) > 0.9
face_c = (fc[sel]*fa[sel,None]).sum(0)/fa[sel].sum()
def text_mesh(s):
tp = TextPath((0,0), s, size=TEXT_H, prop=FontProperties(family='DejaVu Sans'))
rings = [ShPoly(p) for p in tp.to_polygons() if len(p)>=3]
rings.sort(key=lambda r:r.area, reverse=True)
used=[False]*len(rings); parts=[]
for i,o in enumerate(rings):
if used[i]: continue
holes=[]
for j in range(i+1,len(rings)):
if not used[j] and o.contains(rings[j]): holes.append(rings[j].exterior.coords); used[j]=True
parts.append(ShPoly(o.exterior.coords,holes)); used[i]=True
poly = unary_union(parts)
geoms = list(poly.geoms) if poly.geom_type=='MultiPolygon' else [poly]
m = trimesh.util.concatenate([trimesh.creation.extrude_polygon(g,height=TEXT_DEPTH+TEXT_OVERSHOOT) for g in geoms])
c = m.bounds.mean(axis=0); m.apply_translation([-c[0],-c[1],0]); return m
for t_start, t_end in RANGES:
temps = list(range(t_start, t_end-1, -5))
parts=[]
for i,T in enumerate(temps):
c = unit.copy(); c.apply_translation([0, i*PITCH, 0])
t = text_mesh(str(T)); M=np.eye(4); M[:3,:3]=R; t.apply_transform(M)
# place the text spanning from TEXT_DEPTH inside the face to TEXT_OVERSHOOT outside,
# then CUT it out of the provino (engrave) — no raised material, no Y-overhang.
t.apply_translation(face_c - n*TEXT_DEPTH + np.array([0,i*PITCH,0]))
c = trimesh.boolean.difference([c, t], engine='manifold')
parts.append(c)
asset = trimesh.util.concatenate(parts)
out = os.path.join(HERE, f"belt_temp_tower_{t_start}_{t_end}.stl")
asset.export(out)
dims = np.round(asset.bounds[1]-asset.bounds[0],1)
wt = all(p.is_watertight for p in parts)
print(f" {t_start}->{t_end}: {len(temps)} zones bbox={dims} watertight={wt} -> {os.path.basename(out)}")
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M5.5,14.5c-1.105,0-2-3.686-2-7s.895-7,2-7" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="8.67" y1="4.67" x2="14.33" y2="10.33" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round"/><line x1="14.33" y1="4.67" x2="8.67" y2="10.33" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round"/><path d="M3.5,13.7c-.294.511-.636.8-1,.8-1.1,0-2-3.686-2-7s.9-7,2-7c.365,0,.707.293,1,.805" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M6.727,4.5c.334-2.208,1-4,1.773-4,.354,0,.686.378.974,1" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M9.474,13.5c-.288.622-.62,1-.974,1-.77,0-1.439-1.792-1.773-4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>

Before

Width:  |  Height:  |  Size: 931 B

+3 -1
View File
@@ -1 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3.5,11.5v1l2,2,2-2v-1" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-linejoin:round;"/><circle cx="10.5" cy="4.5" r="4" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="11.5 2.5 12.5 3.5 8.5 3.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="12.5 5.5 8.5 5.5 9.5 6.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-linejoin:round;"/><path d="M5.5.5H1.5C.95.5.5.95.5,1.5v9c0,.55.45,1,1,1h8c.55,0,1-.45,1-1" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-linejoin:round;"/></svg>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2992 1.42627C10.7774 1.42627 8.73315 3.47056 8.73315 5.99231C8.73315 8.51406 10.7774 10.5583 13.2992 10.5583C13.3609 10.5583 13.4223 10.5571 13.4834 10.5547C15.9197 10.4581 17.8652 8.45238 17.8652 5.99231C17.8652 3.47056 15.8209 1.42627 13.2992 1.42627ZM15.6185 11.0536C17.5346 10.1741 18.8652 8.23861 18.8652 5.99231C18.8652 2.91827 16.3732 0.42627 13.2992 0.42627C11.5793 0.42627 10.0417 1.2063 9.02067 2.43188H2.31853C1.93193 2.43188 1.61853 2.74528 1.61853 3.13188V12.4712C1.61853 12.6313 1.6723 12.7789 1.76278 12.8968C1.76671 13.0046 1.79621 13.1144 1.85576 13.2185L3.48408 16.0649C3.60875 16.2829 3.8406 16.4173 4.09168 16.4173H6.75859L7.97903 19.1586C8.22556 19.7123 9.01148 19.7123 9.25801 19.1586L10.4784 16.4173H13.1454C13.3965 16.4173 13.6283 16.2829 13.753 16.0649L15.3813 13.2185C15.4409 13.1144 15.4704 13.0046 15.4743 12.8968C15.5648 12.7789 15.6185 12.6313 15.6185 12.4712V11.0536ZM13.4429 11.5565C13.8469 11.5463 14.2403 11.493 14.6185 11.4011V12.1709H2.61853V3.43188H8.35573C7.95792 4.19837 7.73315 5.06909 7.73315 5.99231C7.73315 9.06635 10.2252 11.5583 13.2992 11.5583C13.3472 11.5583 13.3951 11.5577 13.4429 11.5565ZM2.98076 13.1712H14.2563L12.9714 15.4173H9.85194L9.84578 15.4173H7.39126L7.3851 15.4173H4.26569L2.98076 13.1712ZM9.38381 16.4173L8.61852 18.1363L7.85322 16.4173H9.38381ZM12.2021 3.36422C12.3973 3.55948 12.3973 3.87606 12.2021 4.07133L11.743 4.53041H16.0625C16.3386 4.53041 16.5625 4.75426 16.5625 5.03041C16.5625 5.30655 16.3386 5.53041 16.0625 5.53041H10.7773C10.2428 5.53041 9.97507 4.88412 10.353 4.50614L11.495 3.36422C11.6902 3.16896 12.0068 3.16896 12.2021 3.36422ZM14.3963 7.91305C14.2011 8.10831 14.2011 8.42489 14.3963 8.62015C14.5916 8.81542 14.9082 8.81542 15.1034 8.62015L16.2453 7.47823C16.6233 7.10026 16.3556 6.45397 15.8211 6.45397L10.5359 6.45397C10.2597 6.45397 10.0359 6.67783 10.0359 6.95397C10.0359 7.23011 10.2597 7.45397 10.5359 7.45397L14.8554 7.45397L14.3963 7.91305Z" fill="#262E30"/>
</svg>

Before

Width:  |  Height:  |  Size: 747 B

After

Width:  |  Height:  |  Size: 2.1 KiB

+10 -1
View File
@@ -1 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3.5,3.5h8M3.5,5.5h8M3.5,1.5h8M6.5,5.5v7l1,1,1-1v-7" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-linejoin:round;"/></svg>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.73486" y="1" width="10.53" height="5.12169" fill="#C2C2C2"/>
<rect x="1.73486" y="1" width="10.53" height="0.497608" fill="#858585"/>
<rect x="1.73486" y="2.70703" width="10.53" height="0.497608" fill="#858585"/>
<rect x="1.73486" y="4.41602" width="10.53" height="0.497608" fill="#858585"/>
<rect x="1.73486" y="6.12305" width="10.53" height="0.497608" fill="#858585"/>
<path d="M6.10107 11.9961H7.77047L7.30675 12.9991H6.61117L6.10107 11.9961Z" fill="#8C8C8C"/>
<rect x="4.1748" y="6.62109" width="5.65026" height="5.3765" fill="#646464"/>
<rect x="6.61475" y="6.62109" width="0.642075" height="5.35947" fill="#1D1D1D"/>
</svg>

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 737 B

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,7 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#545454" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#545454" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#545454" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#545454" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#545454" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#545454" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#c4c4c4" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#c4c4c4" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#c4c4c4" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#c4c4c4" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#c4c4c4" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#c4c4c4" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#009688" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,10 +0,0 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#009688" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

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