Compare commits

...
Author SHA1 Message Date
Ian Chua 173706750d Merge branch 'main' into feat/ota-opc-ci 2026-09-15 15:19:43 +08:00
HanifKoh 0956b4d8fe Add a Nightly Parity Workflow (#15712)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Adds a nightly workflow that runs the long parity checks from
[orca-test-repo](https://github.com/OrcaSlicer/orca-test-repo), which
are too slow for the per-build "Run external slicer regression tests"
step and are kept out of every PR and merge build.

## What it runs

`.github/workflows/parity_nightly.yml`, three jobs:

| Job | What it does |
|---|---|
| Find the build to test | Picks the latest successful `build_all.yml`
run for the branch (`main` by default) and records its commit. |
| Override sweep effect stage (shard 0 and 1) | Runs orca-test-repo's
override sweep with `--effect-full`: every config option that lands on
the CLI is re-sliced on its own to check that it actually changes the
G-code. Split into 2 shards, each with a 60-minute timeout. |
| GUI-vs-CLI parity harness | Slices a set of fixtures in the GUI
(headless under Xvfb) and on the CLI, compares the exports, and scores
divergences against a known-differences ledger. It reports only and
never fails on a divergence. |

## When it runs

- **Every night at 21:00 UTC,** after `build_all.yml`'s 17:00 UTC run
has finished.
- **By hand** through `workflow_dispatch`, with optional inputs:
  - `build_branch`: the branch whose latest successful build to test;
  - `test_repo_ref`: the orca-test-repo ref;
  - `fixtures`: a subset of harness fixtures;
  - `cli_presets`: `flat` or `raw`.
- **No `push` or `pull_request` trigger,** so nothing here runs on PRs
or merges. The per-build CI step is unchanged.

## How it tests a build

- **Binary:** the Linux x86_64 AppImage from the chosen `build_all` run.
- **Source:** OrcaSlicer checked out at that run's exact commit. The
AppImage only ships packed preset caches, so profiles and the CLI option
list come from this checkout, matched to the binary.
- **Output:** each job writes a summary to the run page and uploads its
report (`override-report-shard*`, `parity-scorecard`) for 30 days.
- **Failures:** a failing effect shard fails the run, and GitHub's usual
failure notification for scheduled workflows applies.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

- Dispatched on this branch against orca-test-repo `main` and #15693's
build ([run
34933239912](https://github.com/OrcaSlicer/OrcaSlicer/actions/runs/34933239912)).
Every job passed:
- **effect shard 0:** 19m31s; 225 options sliced, 151 effective, none
crashed or hung, every fixture sliced;
  - **effect shard 1:** 19m33s; 349 options sliced, 254 effective, same;
  - **harness:** 4m32s; all 13 fixtures, 0 new divergences, 0 errors.
- An earlier dispatch on this branch, testing #15693's build against
orca-test-repo's parity branch ([run
34836468900](https://github.com/OrcaSlicer/OrcaSlicer/actions/runs/34836468900)),
passed: effect shards in 20m28s and 23m30s, and the harness reported 0
new divergences.
- The workflow only runs from the default branch on its schedule, so the
nightly trigger itself takes effect once this is merged.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-15 15:01:35 +08:00
HanifKoh 5514559feb Load Each Vendor Tree Once When the CLI Resolves System Presets (#15693)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Since #15438, every CLI run that loads a system preset spends about a
second per preset file re-parsing that vendor's entire profile tree. A
slice with a machine, process and filament preset got roughly 2.5 s
slower, and a four-filament slice roughly 4 s slower. This PR loads each
vendor tree once per run instead. Resolved presets and G-code are
unchanged.

The GUI never takes this path, and no release contains #15438, so the
regression only affects CLI runs on current dev and nightly builds. That
includes print farms, slicing services and plugins that call
`orca-slicer --slice`, and CI suites.

## Changes

### Why it was slow

`PresetBundle::resolve_preset_config` resolves a system preset through
its vendor manifest by loading the whole OrcaFilamentLibrary bundle and
the whole vendor tree from JSON, then picking the one preset out. The
CLI did that separately for every `--load-settings` and
`--load-filaments` file, on a fresh `PresetBundle` each time. With BBL
presets, a machine + process + filament run opened `BBL.json` three
times and read BBL's 2,879 profile files and the library's 512 three
times over.

### Load each vendor tree once

- `PresetBundle` keeps every vendor bundle its manifest path loads,
keyed by source root, vendor and substitution rule, and reuses them for
later resolutions on the same bundle.
- OrcaFilamentLibrary is cached the same way, so vendors under one root
share a single library load and the library's own presets resolve from
that same instance. A vendor bundle only reads from its base while
loading, so sharing it is safe.
- A failed or throwing load is not kept, so error reporting is
unchanged.
- The key includes the source root, so presets from two different
profile roots still resolve separately.
- The CLI resolves every system preset through one `PresetBundle` for
the whole run, instead of creating one per file.

The resolved configurations still come from the same canonical vendor
loader, so what a preset resolves to does not change. Only the CLI calls
`resolve_preset_config`, so a long-lived GUI bundle cannot end up
holding profile trees that later change on disk.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

CLI slice of a 20 mm cube with X1 Carbon system presets. Both builds get
the same datadir, best of 3, Linux. "Before" is this PR's base from CI.

| System presets loaded | Before | After |
|---|---|---|
| machine | 0.95 s | 0.87 s |
| machine + process | 1.67 s | 0.92 s |
| machine + process + 1 filament | 2.51 s | 0.97 s |
| machine + process + 4 filaments | 5.00 s | 0.99 s |

Files opened during the machine + process + filament run (`strace -e
openat`):

| | Before | After |
|---|---|---|
| `BBL.json` | 3 | 1 |
| `OrcaFilamentLibrary.json` | 3 | 1 |
| `system/BBL/**/*.json` | 8,634 | 2,880 |
| `system/OrcaFilamentLibrary/**/*.json` | 1,536 | 512 |

Peak memory did not rise: max RSS 306 MB → 286 MB for the three-preset
run, and 305 MB → 285 MB for four filaments. The "before" figure is an
AppImage, so part of that gap is probably packaging.

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

- New test "Manifest-backed resolution reuses the vendor tree it already
loaded" in `tests/libslic3r/test_preset_bundle_loading.cpp`. It resolves
one preset, changes the parent profile on disk, then resolves a sibling.
The same bundle returns the value it already loaded, and a fresh bundle
picks up the change.
- New test "Manifest-backed resolution shares the library between
vendors under one root". It resolves through one vendor, changes a
library profile on disk, then resolves through a second vendor and a
library preset on the same bundle. Both return the value already loaded,
and a fresh bundle picks up the change.
- All `[Preset][Bundle]` tests pass (87 test cases, 1,069 assertions),
including the existing manifest-backed resolution cases for source-root
scoping, malformed vendor loads, missing parents and type mismatches.
- G-code of the three-preset slice is identical before and after, header
lines excluded.
- The external CLI regression suite passes. Two cases report as
unexpectedly passing because #15639 fixed their bug. They pass the same
way on this PR's base without the change.
- A GUI-vs-CLI parity run over 10 fixtures shows no new differences.
- Builds clean on Linux (Release, with tests).

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-15 15:01:24 +08:00
Hanif Koh d5cf1502c4 Share One Library Load Between Vendors in the CLI Preset Resolver
The manifest resolver loaded OrcaFilamentLibrary once per vendor it
resolved through, so a run that mixes vendors parsed the library tree
again for each of them. The library is now cached like any other vendor
tree, keyed on its root and substitution rule, and doubles as the base
every vendor under that root loads against. A vendor bundle only reads
from its base while loading, so sharing the instance is safe.

The cache key carries the substitution rule as its enum, and the lookup
lambdas take a const bundle since they only read.
2026-09-15 13:31:30 +08:00
HanifKoh 37e2b6c928 CLI: let --export-settings - write the merged config JSON to stdout (#15698)
`--export-settings` already writes the merged config as JSON at the
right point in the CLI flow. Passing `-` now writes that same document
to stdout, so scripts can inspect the effective config without a temp
file. This replaces #14605.

- `ConfigBase::save_to_json` gains a stream overload. The file overload
serializes through it before opening the file, so the output format is
unchanged, and a config that cannot be serialized (invalid UTF-8) now
leaves the existing file untouched instead of truncating it.
- On stdout, invalid UTF-8 in string values is written as U+FFFD instead
of ending the process with an uncaught `type_error`. Files keep the
strict behaviour.
- To keep stdout pure JSON, `-` is rejected up front (stderr message,
`CLI_INVALID_PARAMS`, shell status 254) when combined with an action or
transform that can write to stdout or does real work: `--info`,
`--help`, `--orient`, slicing and exporting. Options that do nothing
without a slice (`--uptodate`, `--min-save`, `--pipe`, ...) are still
accepted.
- The one unconditional stdout write on a success path, "skip locked
instance" during arrange, now goes to the log.
- Every other value, including the default `output.json`, behaves as
before.

Tests in `tests/libslic3r/test_config.cpp`: the stream output equals the
file output and keeps the tab-indented format; invalid UTF-8 throws on
the strict path and is replaced when asked; a failed save leaves the
previous file intact.
2026-09-15 13:16:22 +08:00
Kris Austin efc9f253ee fix: resolve relative input paths given on the command line (#14803)
Opening a model with a relative path, for example `orca-slicer ./some.3mf`,
failed with "Loading of a model file failed." and "The file does not contain
any geometry data.", while the same file opened by an absolute path or by
drag and drop worked.

GUI_App::init_app_config() changes the working directory to <data_dir>/log,
and it runs from the GUI_App constructor because the app config is needed
early for instance checking. The input files are opened much later, in
post_init(), so a path still relative at that point resolved against the log
directory instead of the directory OrcaSlicer was started from, and the 3MF
reader failed to open it.

Resolve the input paths in CLI::setup(), which runs before GUI_App is
constructed and therefore before the working directory moves. Absolute paths
are returned unchanged, so the forms that open today are unaffected, and
custom open protocol URLs are passed through since post_init() hands those to
the downloader rather than the file loader.

The working directory change is left alone. It was added in #3248 so the TUTK
logs land in the data directory instead of the working directory (#3209).
2026-09-15 12:47:01 +08:00
Kris Austin 292cf0095e drop the per-frame mouse raycast that only a drag start reads (#15664) 2026-09-14 18:31:23 -03:00
Kris Austin 5c635d5e50 build: scope -Werror to the Clang family so GCC builds again (#15701) 2026-09-14 17:04:03 -03:00
Noprazandyw4z 5496883493 fix(profiles): Snapmaker U1 — cap ABS/ASA/PPS bed temps at 100 °C (#15483)
The U1's heated bed tops out at 100 °C, but these profiles requested
105-110 °C, which leads to print errors unless the user modifies the
printer's firmware configuration.

Affected profiles:
- Snapmaker ABS @U1 base (110/105 → 100)
- Snapmaker ASA @U1 base (110 → 100)
- Fiberon ASA-CF08 @Snapmaker U1 base (105 → 100)
- Fiberon PPS-GF20 @Snapmaker U1 base (105 → 100)

Bumps Snapmaker.json to 02.04.00.10.

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-09-14 20:44:15 +03:00
packerlschupfer 31eb8a2bd1 CLI: let --export-settings - write the merged config to stdout
--export-settings already writes the merged config as JSON at the right
point in the CLI flow. Passing - writes the same document to stdout.

- ConfigBase::save_to_json gains a stream overload. The file overload
  serializes through it before opening the file, so the format is
  unchanged and a config that cannot be serialized leaves an existing
  file untouched instead of truncating it.
- On stdout, invalid UTF-8 in string values is written as U+FFFD instead
  of ending the process with an uncaught type_error; files keep the
  strict behaviour.
- - is rejected up front when combined with an action or transform that
  can write to stdout or does real work, so stdout carries only the
  JSON.
- The unconditional "skip locked instance" stdout write during arrange
  now goes to the log.
- Tests in tests/libslic3r/test_config.cpp.
2026-09-14 19:35:29 +02:00
Daniel Williams 70247ad298 Extract Layer::choose_ironing_extruder for unit-testable ironing routing (#13467)
* Extract Layer::choose_ironing_extruder for unit-testable ironing routing

The ironing extruder selection in make_ironing() was a 5-line nested
conditional inlined at the top of the loop, with no isolated test
coverage. Pull the gating into a static helper so the routing decision
is unit-testable without spinning up the slicing pipeline.

Pure refactor: the helper preserves the original logic bit-for-bit
(NoIroning -> -1; AllSolid always enabled; TopSurfaces and TopmostOnly
require some top shells or, in spiral mode, more than one bottom shell;
TopmostOnly additionally requires being on the topmost layer; enabled
ironing routes to solid_infill_filament).

Add tests/fff_print/test_choose_ironing_extruder.cpp covering:
- AllSolid regardless of layer position
- TopSurfaces with top_shell_layers > 0
- TopSurfaces with top_shell_layers=0 + spiral mode + bottom_shell_layers>1
- TopmostOnly + topmost layer
- NoIroning short-circuit
- TopSurfaces with top_shell_layers=0 (and not spiral) -> disabled
- TopSurfaces, spiral, but bottom_shell_layers=1 -> disabled
- TopmostOnly on a non-topmost layer -> disabled

* Move ironing routing test into the Fill subsystem file

Rename the test to tests/libslic3r/test_fill.cpp and tag it [Fill] to
match the subsystem it covers, use flat behavioral test cases with
GENERATE for the parameterized ones, and drop the history narration from
the code comments.

* tests: move ironing routing tests into fff_print/test_fill.cpp

Keeps the Fill tests in one file, alongside the existing ironing
rotation-template test.
2026-09-14 09:37:04 -03:00
Hanif Koh d4840901fc Test That Failed Vendor Loads Are Not Kept and the Library Base Is Reused
Cover the two cache paths the first test left open: a vendor tree that fails to load is retried on the next resolution instead of being served from the cache, and a type-probed filament resolved through resolve_preset_config_type reuses the OrcaFilamentLibrary base already loaded for a sibling.
2026-09-14 18:51:48 +08:00
Hanif Koh 5f01f21661 Load Each Vendor Tree Once When the CLI Resolves System Presets
Resolving a system preset through its vendor manifest loaded the whole vendor tree and the filament library from JSON, and the CLI did that separately for every --load-settings and --load-filaments file. A run with machine, process and filament presets parsed BBL's 2,879 profile files and the library's 512 three times over, about a second each.

Keep the library and vendor bundles loaded by the manifest path on the PresetBundle that resolved them, keyed by source root, vendor and substitution rule, and have the CLI resolve every system preset through one bundle for the whole run. A failed load is not kept, so errors are reported as before.

On a cube slice with X1C machine, process and PLA presets: 2.42 s -> 0.93 s, BBL.json opened once instead of three times, identical G-code.
2026-09-14 17:44:17 +08:00
Lam Wei Lun ecbe1b1b90 UI Bug fixes and code cleanup for Publish 3MF Dialog (#15690)
# Description
- Fixes an issue on macOS where the modified indicator can be cut-off.
- Remove unused code
2026-09-14 16:57:41 +08:00
Lam Wei Lun ffb4f192c1 Fix macOS UI issue in publish dialog. Remove item_size helper in TabCtrl and its relevant setter 2026-09-14 14:19:25 +08:00
Hanif Koh 4373bc3697 Add a Nightly Parity Workflow
Runs orca-test-repo's full override-sweep effect stage (two shards) and the GUI-vs-CLI parity harness every night against the latest successful build_all Linux AppImage, with sources checked out at that build's commit. Kept out of the per-build regression step, whose time budget it would exceed, and never gates a build.
2026-09-14 13:38:51 +08:00
Ian Chua ffb2946b20 feat: compare with environment variable FOLDER_MERGERS for verified folders 2026-09-10 20:09:37 +08:00
Ian Chua e89a05b1e4 fix: repo name 2026-09-10 16:38:03 +08:00
Ian Chua 02a8011b97 feat: add CI to generate OPC for OTA workflow 2026-09-10 16:27:23 +08:00
31 changed files with 1097 additions and 125 deletions
+23
View File
@@ -684,6 +684,18 @@ jobs:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator' path: './build/src/Release/OrcaSlicer_profile_validator'
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
# <vendor>.opc caches with; it was already built by the "Build system
# preset cache (Linux)" step above. The .opc format is 64-bit
# little-endian native, i.e. identical across every platform Orca ships,
# so only the Linux binary is published.
- name: Upload generate_system_cache Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: actions/upload-artifact@v7
with:
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
@@ -714,6 +726,17 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
max_releases: 1 max_releases: 1
- name: Deploy Ubuntu generate_system_cache release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0
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 - name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
+219
View File
@@ -0,0 +1,219 @@
# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the
# per-build "Run external slicer regression tests" step because they take far
# longer than that step's budget:
# effect - the CLI override sweep's full effect stage: every landed option
# re-sliced on its own to see whether it changes the G-code
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
# Both test the latest successful build_all.yml Linux AppImage from main, with
# sources checked out at the commit that build was made from. Nothing here
# gates a build or a PR.
name: Parity Nightly
on:
schedule:
# build_all.yml starts at 17:00 UTC and has finished by ~20:00
- cron: "0 21 * * *"
workflow_dispatch:
inputs:
test_repo_ref:
description: "orca-test-repo ref to run"
required: false
default: "main"
build_branch:
description: "branch whose latest successful build_all artifact to test"
required: false
default: "main"
fixtures:
description: "harness fixture ids, space-separated (empty = all)"
required: false
default: ""
cli_presets:
description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is"
required: false
default: "flat"
permissions:
contents: read
actions: read
jobs:
build:
name: Find the build to test
# Don't run scheduled checks on forks
if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.find.outputs.run_id }}
head_sha: ${{ steps.find.outputs.head_sha }}
steps:
- id: find
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run list --workflow build_all.yml \
--branch "${{ inputs.build_branch || 'main' }}" \
--status success --limit 1 --json databaseId,headSha \
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
>> "$GITHUB_OUTPUT"
cat "$GITHUB_OUTPUT"
effect:
name: Override sweep effect stage (shard ${{ matrix.shard }})
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
# orca-test-repo's parity/effect_routing.json holds a 2-way split,
# ~12.5 min a shard on this runner
shard: [0, 1]
steps:
- &checkout-suite
name: Check out the test suite
uses: actions/checkout@v7
with:
repository: OrcaSlicer/orca-test-repo
ref: ${{ inputs.test_repo_ref || 'main' }}
path: orca-test-repo
# The AppImage ships only packed preset caches, so profiles and the CLI
# option surface come from the sources the build was made from
- &checkout-slicer
name: Check out OrcaSlicer at the build's commit
uses: actions/checkout@v7
with:
ref: ${{ needs.build.outputs.head_sha }}
path: slicer
lfs: 'false'
- &extract-appimage
name: Download and extract the Linux AppImage
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
[ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; }
chmod +x "$appimage"
"$appimage" --appimage-extract > /dev/null
# The bare binary cannot find the AppImage's bundled libraries; AppRun
# sets them up and execs it, so exit codes and signals pass through
[ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; }
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
- name: Install the AppImage's host runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install suite dependencies
run: pip install -r orca-test-repo/requirements.txt
- name: Run the override sweep with the full effect stage
id: run
continue-on-error: true
working-directory: orca-test-repo
run: |
set -o pipefail
# -rA keeps the per-stage summaries, which pytest otherwise swallows
# for passing tests
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \
--effect-full --effect-shard ${{ matrix.shard }}/2 \
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
2>&1 | tee ../sweep.log
- name: Publish job summary
if: always()
run: |
{
echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2"
echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})"
echo '```'
grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log"
grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the override report
if: always()
uses: actions/upload-artifact@v7
with:
name: override-report-shard${{ matrix.shard }}
path: |
orca-test-repo/.pytest_cache/override_report.json
sweep.log
if-no-files-found: warn
retention-days: 30
# The sweep step continues on error so the summary and report still get
# published; this puts the failure back on the job
- name: Fail the job if the sweep failed
if: steps.run.outcome == 'failure'
run: |
echo "the override sweep failed, see the job summary and the uploaded report" >&2
exit 1
harness:
name: GUI-vs-CLI parity harness
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- *checkout-suite
- *checkout-slicer
- *extract-appimage
- name: Install display tooling and the AppImage's host runtime
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness
run: |
set -euo pipefail
fixtures=()
for f in ${{ inputs.fixtures || '' }}; do
fixtures+=(--fixture "$f")
done
# 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner,
# and each fixture is fully isolated, so results match a serial run
python3 orca-test-repo/parity/run_parity.py \
--slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \
--cli-presets "${{ inputs.cli_presets || 'flat' }}" \
--gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}"
- name: Publish job summary
if: always()
run: |
if [ -f parity-out/report.md ]; then
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Drop per-lane datadirs before upload
if: always()
run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true
- name: Upload the scorecard and evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: parity-scorecard
path: parity-out/
if-no-files-found: warn
retention-days: 30
+253
View File
@@ -0,0 +1,253 @@
name: Post-merge profiles
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
# profile change lands on main or a release branch, rebuild the affected vendors'
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
# only then does it become a live OTA update - this workflow does none of that
# last part (no changelog, no R2, no webhook).
#
# Asset contract expected by OrcaCloud's release scanner:
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
#
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
on:
push:
branches:
- main
- release/*
paths:
- 'resources/profiles/**'
- '.github/workflows/post_merge_profiles.yml'
workflow_dispatch:
permissions:
contents: read
# One run per branch; let a run finish rather than cancel it, since it publishes.
concurrency:
group: post-merge-profiles-${{ github.ref }}
cancel-in-progress: false
env:
# generate_system_cache is published to this repo's own nightly-builds release
# by build_orca.yml's Linux leg. The job guard pins github.repository to
# OrcaSlicer/OrcaSlicer, so this resolves there.
TOOL_REPO: ${{ github.repository }}
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
PROFILES_OWNER: OrcaSlicer
PROFILES_REPO: orcaslicer-profiles
jobs:
publish_profile_caches:
name: Publish profile caches
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
# merge bot. Keep this environment free of protection rules so this
# push-triggered job does not wait for a reviewer.
environment: merge-delegation
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Enough history to reach github.event.before for the changed-vendor
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
fetch-depth: 50
- name: Resolve changed vendors
id: vendors
shell: bash
env:
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
run: |
set -euo pipefail
base='${{ github.event.before }}'
head='${{ github.sha }}'
# Zero SHA (branch created / force push) or manual dispatch: fall back
# to this commit's own diff.
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
base="$head^"
fi
echo "Diffing $base..$head"
mapfile -t candidates < <(
git diff --name-only "$base" "$head" -- resources/profiles \
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
| sort -u
)
vendors=()
for v in "${candidates[@]:-}"; do
[ -n "$v" ] || continue
json="resources/profiles/$v.json"
# A vendor has a manifest plus either a preset directory or a version
# field; this drops non-vendor files such as blacklist.json.
if [ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }; then
vendors+=("$v")
fi
done
if [ "${#vendors[@]}" -eq 0 ]; then
echo "No changed vendor profiles in this push; nothing to publish."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
# A vendor is eligible only when both the profile directory and its
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
# grant. The account part is intentionally ignored here: this is a
# post-merge safety check, not an authorization check for a command.
grants=()
while IFS= read -r raw_line; do
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
[[ "$line" == *:* ]] || continue
grant="${line#*:}"
grant="${grant#"${grant%%[![:space:]]*}"}"
grant="${grant%"${grant##*[![:space:]]}"}"
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
grants+=("$grant")
done <<< "${FOLDER_MERGERS:-}"
is_granted() {
local path="$1"
local grant
for grant in "${grants[@]:-}"; do
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
return 0
fi
done
return 1
}
unauthorized=()
for v in "${vendors[@]}"; do
if ! is_granted "resources/profiles/$v" || ! is_granted "resources/profiles/$v.json"; then
unauthorized+=("$v")
fi
done
if [ "${#unauthorized[@]}" -ne 0 ]; then
echo "Changed vendor profiles are not covered by FOLDER_MERGERS: ${unauthorized[*]}"
echo "No profile caches will be published for this push."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf 'Changed vendors: %s\n' "${vendors[*]}"
echo "vendors=${vendors[*]}" >> "$GITHUB_OUTPUT"
- name: Resolve Orca version
id: orca
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
orca_ver="${raw%%-*}"
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
fi
# release_tag is what the desktop client sends as orca_version and what
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
echo "Orca version: release_tag=$raw asset_prefix=$orca_ver"
- name: Validate profile versions
id: pver
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
: > "$RUNNER_TEMP/pver.tsv"
for v in ${{ steps.vendors.outputs.vendors }}; do
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
exit 1
fi
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
echo "$v -> $pv"
done
- name: Download generate_system_cache
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
# gh (with the default token) rather than an unauthenticated curl: keeps
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release download nightly-builds --repo "$TOOL_REPO" \
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
chmod +x generate_system_cache
- name: Build caches and package assets
id: pkg
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
# One timestamp for the whole run so a multi-vendor merge groups together.
ts="$(date -u +%Y%m%d%H%M)"
orca_ver='${{ steps.orca.outputs.orca_ver }}'
out="$RUNNER_TEMP/assets"
mkdir -p "$out"
for v in ${{ steps.vendors.outputs.vendors }}; do
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
opc="resources/profiles/$v.opc"
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
name="${orca_ver}_${v}_${pv}_${ts}.zip"
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
echo "packaged $name"
done
echo "dir=$out" >> "$GITHUB_OUTPUT"
- name: Mint profiles-repo token
id: token
if: steps.vendors.outputs.vendors != ''
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PROFILES_APP_ID }}
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
owner: ${{ env.PROFILES_OWNER }}
repositories: ${{ env.PROFILES_REPO }}
- name: Publish assets to profiles release
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
run: |
set -euo pipefail
repo="$PROFILES_OWNER/$PROFILES_REPO"
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
echo "Creating release $RELEASE_TAG on $repo"
gh release create "$RELEASE_TAG" --repo "$repo" \
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
--latest=false
fi
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
{
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
} >> "$GITHUB_STEP_SUMMARY"
+9 -18
View File
@@ -587,10 +587,15 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
add_compile_options(-Wno-${w}) add_compile_options(-Wno-${w})
endforeach () endforeach ()
# Turn everything else into an error. Dependency headers are exempt because the SYSTEM # GCC is not built in CI, so don't throw errors CI won't catch.
# include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their diagnostics out, if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# apart from GCC's maybe-uninitialized, demoted below. add_compile_options(-Werror=return-type)
add_compile_options(-Werror) else ()
# Turn everything else into an error. Dependency headers are exempt because the
# SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their
# diagnostics out.
add_compile_options(-Werror)
endif ()
# Demoted. Remove a name once its category is cleared on every compiler. # Demoted. Remove a name once its category is cleared on every compiler.
set(warnings_demoted) set(warnings_demoted)
@@ -612,20 +617,6 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
cast-function-type-mismatch cast-function-type-mismatch
) )
endif () endif ()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND warnings_demoted
# maybe-uninitialized runs after inlining and reports inside boost/variant,
# boost/tuple and the bundled clipper header even with -isystem.
maybe-uninitialized
# array-bounds is reported once, where ConfigOptionVector::set_at inlines
# into OrcaSlicer.cpp on a branch the preceding type test rules out.
array-bounds
# template-id-cdtor is a GCC 14+ warning in the bundled Clipper2 headers.
template-id-cdtor
)
endif ()
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND warnings_demoted list(APPEND warnings_demoted
# enum-constexpr-conversion is a Clang warning that defaults to an error, # enum-constexpr-conversion is a Clang warning that defaults to an error,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "Snapmaker", "name": "Snapmaker",
"version": "02.04.00.12", "version": "02.04.00.13",
"force_update": "0", "force_update": "0",
"description": "Snapmaker configurations", "description": "Snapmaker configurations",
"machine_model_list": [ "machine_model_list": [
@@ -15,13 +15,13 @@
"1" "1"
], ],
"cool_plate_temp": [ "cool_plate_temp": [
"105" "100"
], ],
"cool_plate_temp_initial_layer": [ "cool_plate_temp_initial_layer": [
"105" "100"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
"105" "100"
], ],
"eng_plate_temp_initial_layer": [ "eng_plate_temp_initial_layer": [
"100" "100"
@@ -48,7 +48,7 @@
"Polymaker" "Polymaker"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"105" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "100"
@@ -72,7 +72,7 @@
"110.8" "110.8"
], ],
"textured_plate_temp": [ "textured_plate_temp": [
"105" "100"
], ],
"textured_plate_temp_initial_layer": [ "textured_plate_temp_initial_layer": [
"100" "100"
@@ -15,16 +15,16 @@
"1" "1"
], ],
"cool_plate_temp": [ "cool_plate_temp": [
"105" "100"
], ],
"cool_plate_temp_initial_layer": [ "cool_plate_temp_initial_layer": [
"105" "100"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
"105" "100"
], ],
"eng_plate_temp_initial_layer": [ "eng_plate_temp_initial_layer": [
"105" "100"
], ],
"fan_cooling_layer_time": [ "fan_cooling_layer_time": [
"12" "12"
@@ -51,10 +51,10 @@
"Polymaker" "Polymaker"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"105" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"105" "100"
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"300" "300"
@@ -81,10 +81,10 @@
"110" "110"
], ],
"textured_plate_temp": [ "textured_plate_temp": [
"105" "100"
], ],
"textured_plate_temp_initial_layer": [ "textured_plate_temp_initial_layer": [
"105" "100"
], ],
"filament_type": [ "filament_type": [
"ABS" "ABS"
@@ -9,10 +9,10 @@
"" ""
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"110" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"105" "100"
], ],
"overhang_fan_speed": [ "overhang_fan_speed": [
"20" "20"
@@ -9,7 +9,7 @@
"" ""
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"110" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "100"
+38 -6
View File
@@ -1387,6 +1387,25 @@ int CLI::run(int argc, char **argv)
if (downward_check_option) if (downward_check_option)
downward_check = downward_check_option->value; downward_check = downward_check_option->value;
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
// sliced or exported.
if (std::find(m_actions.begin(), m_actions.end(), "export_settings") != m_actions.end() && m_config.opt_string("export_settings") == "-") {
static const std::set<std::string> stdout_compatible = { "export_settings", "uptodate", "load_defaultfila", "min_save",
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
for (const std::vector<std::string> *opt_keys : { &m_actions, &m_transforms }) {
for (const std::string &opt_key : *opt_keys) {
if (stdout_compatible.count(opt_key) == 0) {
std::string flag = opt_key;
std::replace(flag.begin(), flag.end(), '_', '-');
boost::nowide::cerr << "--export-settings - cannot be combined with --" << flag << std::endl;
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
flush_and_exit(CLI_INVALID_PARAMS);
}
}
}
}
bool start_gui = m_actions.empty() && !downward_check; bool start_gui = m_actions.empty() && !downward_check;
if (start_gui) { if (start_gui) {
BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl; BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl;
@@ -2010,19 +2029,21 @@ int CLI::run(int argc, char **argv)
} }
}; };
auto resolve_preset = [&ensure_cli_preset_bundle](const std::string &file, DynamicPrintConfig &config, // One resolver for the whole run, so presets from the same vendor tree share its load.
std::unique_ptr<PresetBundle> system_preset_resolver;
auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
std::string &config_type, const std::string &config_from, std::string &config_type, const std::string &config_from,
bool probe_type, std::string &error) { bool probe_type, std::string &error) {
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS); const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
if (!probe_type && (inherits == nullptr || inherits->value.empty())) if (!probe_type && (inherits == nullptr || inherits->value.empty()))
return true; return true;
std::unique_ptr<PresetBundle> source_bundle;
PresetBundle *bundle = nullptr; PresetBundle *bundle = nullptr;
bool allow_source_manifest = false; bool allow_source_manifest = false;
if (config_from == "system") { if (config_from == "system") {
source_bundle = std::make_unique<PresetBundle>(); if (!system_preset_resolver)
bundle = source_bundle.get(); system_preset_resolver = std::make_unique<PresetBundle>();
bundle = system_preset_resolver.get();
allow_source_manifest = true; allow_source_manifest = true;
} else { } else {
bundle = ensure_cli_preset_bundle(error); bundle = ensure_cli_preset_bundle(error);
@@ -5348,7 +5369,7 @@ int CLI::run(int argc, char **argv)
//skip this object due to be locked in plate //skip this object due to be locked in plate
ap.itemid = locked_aps.size(); ap.itemid = locked_aps.size();
locked_aps.emplace_back(ap); locked_aps.emplace_back(ap);
boost::nowide::cout <<__FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
} }
} }
} }
@@ -5937,7 +5958,11 @@ int CLI::run(int argc, char **argv)
//FIXME check for mixing the FFF / SLA parameters. //FIXME check for mixing the FFF / SLA parameters.
// or better save fff_print_config vs. sla_print_config // or better save fff_print_config vs. sla_print_config
//m_print_config.save(m_config.opt_string("save")); //m_print_config.save(m_config.opt_string("save"));
m_print_config.save_to_json(m_config.opt_string(opt_key), std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION)); const std::string &settings_file = m_config.opt_string(opt_key);
if (settings_file == "-")
m_print_config.save_to_json(boost::nowide::cout, "project_settings", "project", SoftFever_VERSION, /*replace_invalid_utf8=*/true);
else
m_print_config.save_to_json(settings_file, std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
} else if (opt_key == "info") { } else if (opt_key == "info") {
// --info works on unrepaired model // --info works on unrepaired model
for (Model &model : m_models) { for (Model &model : m_models) {
@@ -7715,6 +7740,13 @@ bool CLI::setup(int argc, char **argv)
this->print_help(); this->print_help();
return false; return false;
} }
// Orca: resolve here, while the process is still in the directory the user invoked it from.
// GUI_App's constructor moves the working directory to <data_dir>/log, long before the GUI
// opens these files in post_init(), and a relative path would then resolve against that.
for (std::string &input_file : m_input_files)
input_file = resolve_cli_input_path(input_file);
// Parse actions and transform options. // Parse actions and transform options.
for (auto const &opt_key : opt_order) { for (auto const &opt_key : opt_order) {
if (cli_actions_config_def.has(opt_key)) if (cli_actions_config_def.has(opt_key))
+16 -2
View File
@@ -23,7 +23,8 @@ int main(int argc, char* argv[])
#else #else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory") ("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif #endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)"); ("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)")
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional; generate the cache for this vendor only (the Orca filament library is always included as the inheritance base). All vendors if not specified.");
// clang-format on // clang-format on
po::variables_map vm; po::variables_map vm;
@@ -38,6 +39,7 @@ int main(int argc, char* argv[])
const std::string profiles_path = vm["path"].as<std::string>(); const std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>(); const int log_level = vm["log_level"].as<int>();
const std::string vendor = vm["vendor"].as<std::string>();
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) { if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n"; std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
@@ -59,8 +61,12 @@ int main(int argc, char* argv[])
preset_bundle->set_is_validation_mode(true); preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true); preset_bundle->set_default_suppressed(true);
preset_bundle->set_generate_vendor_caches(true); preset_bundle->set_generate_vendor_caches(true);
// Empty == every vendor. Otherwise only this vendor (plus the always-loaded
// Orca filament library) is parsed, so only its <vendor>.opc is written.
preset_bundle->set_vendor_to_validate(vendor);
std::cout << "Loading system presets from: " << profiles_path << "\n"; std::cout << "Loading system presets from: " << profiles_path
<< (vendor.empty() ? "" : " (vendor: " + vendor + ")") << "\n";
try { try {
// In validation mode data_dir() is the profiles directory set above, so the // In validation mode data_dir() is the profiles directory set above, so the
@@ -71,6 +77,14 @@ int main(int argc, char* argv[])
return 1; return 1;
} }
// A specific vendor must have produced its own cache; the always-loaded
// filament library alone would otherwise mask a misspelt or removed name.
if (!vendor.empty() && !fs::exists(fs::path(profiles_path) / (vendor + ".opc"))) {
std::cerr << "No cache was generated for vendor '" << vendor << "' under " << profiles_path
<< " - check the vendor name.\n";
return 1;
}
size_t cache_count = 0; size_t cache_count = 0;
for (auto& entry : fs::directory_iterator(profiles_path)) for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc")) if (boost::iends_with(entry.path().string(), ".opc"))
+15 -6
View File
@@ -7,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include <assert.h> #include <assert.h>
#include <fstream> #include <fstream>
#include <sstream>
#include <iostream> #include <iostream>
#include <iomanip> #include <iomanip>
#include <regex> #include <regex>
@@ -1515,6 +1516,19 @@ std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value
//BBS: add json support //BBS: add json support
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
{
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
std::ostringstream ss;
this->save_to_json(ss, name, from, version);
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << ss.str();
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
}
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
{ {
json j; json j;
//record the headers //record the headers
@@ -1561,12 +1575,7 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
j["plugins"] = unique_refs; j["plugins"] = unique_refs;
} }
boost::nowide::ofstream c; os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl;
c.open(file, std::ios::out | std::ios::trunc);
c << j.dump(1, '\t') << std::endl;
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
} }
void ConfigBase::save(const std::string &file) const void ConfigBase::save(const std::string &file) const
+3
View File
@@ -2825,6 +2825,9 @@ public:
//BBS: add json support //BBS: add json support
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const; void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
// Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless
// replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler).
void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const;
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin // Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json() // dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
+23 -13
View File
@@ -1595,6 +1595,25 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
return sparse_infill_polylines; return sparse_infill_polylines;
} }
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed. AllSolid always irons. TopSurfaces and TopmostOnly need
// either some top shells or, in spiral mode, more than one bottom shell, and
// TopmostOnly additionally needs the layer to be the topmost one.
int Layer::choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer)
{
if (cfg.ironing_type == IroningType::NoIroning)
return -1;
const bool gate = (cfg.ironing_type == IroningType::AllSolid)
|| ((cfg.top_shell_layers > 0 || (spiral_mode && cfg.bottom_shell_layers > 1))
&& (cfg.ironing_type == IroningType::TopSurfaces
|| (cfg.ironing_type == IroningType::TopmostOnly && is_topmost_layer)));
if (!gate)
return -1;
return cfg.top_surface_filament_id;
}
// Create ironing extrusions over top surfaces. // Create ironing extrusions over top surfaces.
void Layer::make_ironing() void Layer::make_ironing()
{ {
@@ -1664,19 +1683,10 @@ void Layer::make_ironing()
if (! layerm->slices.empty()) { if (! layerm->slices.empty()) {
IroningParams ironing_params; IroningParams ironing_params;
const PrintRegionConfig &config = layerm->region().config(); const PrintRegionConfig &config = layerm->region().config();
if (config.ironing_type != IroningType::NoIroning && ironing_params.extruder = Layer::choose_ironing_extruder(
(config.ironing_type == IroningType::AllSolid || config,
((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) && /*spiral_mode=*/this->object()->print()->config().spiral_mode,
(config.ironing_type == IroningType::TopSurfaces || /*is_topmost_layer=*/layerm->layer()->upper_layer == nullptr);
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) {
// Iron the whole face.
ironing_params.extruder = config.top_surface_filament_id;
} else {
// Iron just the infill.
ironing_params.extruder = config.top_surface_filament_id;
}
}
if (ironing_params.extruder != -1) { if (ironing_params.extruder != -1) {
//TODO just_infill is currently not used. //TODO just_infill is currently not used.
ironing_params.just_infill = false; ironing_params.just_infill = false;
+6
View File
@@ -16,6 +16,7 @@ using LayerPtrs = std::vector<Layer*>;
class LayerRegion; class LayerRegion;
using LayerRegionPtrs = std::vector<LayerRegion*>; using LayerRegionPtrs = std::vector<LayerRegion*>;
class PrintRegion; class PrintRegion;
class PrintRegionConfig;
class PrintObject; class PrintObject;
class Print; class Print;
@@ -200,6 +201,11 @@ public:
FillAdaptive::Octree *support_fill_octree, FillAdaptive::Octree *support_fill_octree,
FillLightning::Generator* lightning_generator) const; FillLightning::Generator* lightning_generator) const;
void make_ironing(); void make_ironing();
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed.
static int choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer);
void make_contour_z(const sla::IndexedMesh &mesh); void make_contour_z(const sla::IndexedMesh &mesh);
void export_region_slices_to_svg(const char *path) const; void export_region_slices_to_svg(const char *path) const;
+38 -26
View File
@@ -484,7 +484,7 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem)
compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable;
auto collection_for_type = [](PresetBundle &bundle, Preset::Type preset_type) -> PresetCollection * { auto collection_for_type = [](const PresetBundle &bundle, Preset::Type preset_type) -> const PresetCollection * {
switch (preset_type) { switch (preset_type) {
case Preset::TYPE_PRINT: return &bundle.prints; case Preset::TYPE_PRINT: return &bundle.prints;
case Preset::TYPE_FILAMENT: return &bundle.filaments; case Preset::TYPE_FILAMENT: return &bundle.filaments;
@@ -493,15 +493,15 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
} }
}; };
PresetCollection *collection = collection_for_type(*this, type); const PresetCollection *collection = collection_for_type(*this, type);
if (collection == nullptr) { if (collection == nullptr) {
error = "Unsupported preset type"; error = "Unsupported preset type";
return false; return false;
} }
const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal(); const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal();
auto find_loaded = [&](PresetBundle &bundle) -> const Preset * { auto find_loaded = [&](const PresetBundle &bundle) -> const Preset * {
PresetCollection *loaded_collection = collection_for_type(bundle, type); const PresetCollection *loaded_collection = collection_for_type(bundle, type);
const Preset *resolved = nullptr; const Preset *resolved = nullptr;
for (const Preset &preset : loaded_collection->get_presets()) { for (const Preset &preset : loaded_collection->get_presets()) {
if (preset.file.empty()) if (preset.file.empty())
@@ -549,30 +549,11 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
continue; continue;
try { try {
PresetBundle library_bundle; const PresetBundle *loaded = load_source_vendor(root_dir, vendor_id, compatibility_rule, error);
const PresetBundle *base_bundle = nullptr; if (loaded == nullptr)
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
library_bundle.m_preserve_vendor_source_paths = true;
library_bundle.load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem,
compatibility_rule, nullptr, false);
if (library_bundle.error_count() != 0) {
error = "OrcaFilamentLibrary contains invalid presets";
return false;
}
base_bundle = &library_bundle;
}
PresetBundle source_bundle;
source_bundle.m_preserve_vendor_source_paths = true;
source_bundle.load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem,
compatibility_rule, base_bundle, false);
if (source_bundle.error_count() != 0) {
error = "Vendor bundle contains invalid presets";
return false; return false;
}
const Preset *resolved = find_loaded(source_bundle); const Preset *resolved = find_loaded(*loaded);
if (resolved == nullptr) { if (resolved == nullptr) {
if (error.empty()) if (error.empty())
error = "Source file is not an instantiated preset in its vendor manifest"; error = "Source file is not an instantiated preset in its vendor manifest";
@@ -591,6 +572,37 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
return false; return false;
} }
const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error)
{
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule);
if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end())
return it->second.get();
// The library loads with no base of its own, so the tree a vendor inherits from
// is the same one that resolves the library's own presets.
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error);
if (library == nullptr) {
error = "OrcaFilamentLibrary contains invalid presets";
return nullptr;
}
}
auto bundle = std::make_unique<PresetBundle>();
bundle->m_preserve_vendor_source_paths = true;
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false);
if (bundle->error_count() != 0) {
error = "Vendor bundle contains invalid presets";
return nullptr;
}
return m_source_vendor_bundles.emplace(std::move(key), std::move(bundle)).first->second.get();
}
bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type, bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type,
const std::string &source_file, const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule, ForwardCompatibilitySubstitutionRule compatibility_rule,
+12
View File
@@ -11,6 +11,7 @@
#include <map> #include <map>
#include <set> #include <set>
#include <shared_mutex> #include <shared_mutex>
#include <tuple>
#include <unordered_map> #include <unordered_map>
#include <optional> #include <optional>
#include <array> #include <array>
@@ -652,6 +653,17 @@ private:
bool m_generate_vendor_caches { false }; bool m_generate_vendor_caches { false };
bool m_preserve_vendor_source_paths { false }; bool m_preserve_vendor_source_paths { false };
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
// resolved through this bundle shares one load per source root and vendor. The
// filament library is one such tree, shared by every vendor under its root.
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
m_source_vendor_bundles;
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error);
// Orca: validation only - flag any printer with two or more compatible // Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match). // filament presets sharing one filament_id (ambiguous AMS subtype match).
bool check_duplicate_filament_subtypes() const; bool check_duplicate_filament_subtypes() const;
+1 -1
View File
@@ -11916,7 +11916,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def = this->add("export_settings", coString); def = this->add("export_settings", coString);
def->label = L("Export Settings"); def->label = L("Export Settings");
def->tooltip = L("This exports settings to a file."); def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
def->cli_params = "settings.json"; def->cli_params = "settings.json";
def->set_default_value(new ConfigOptionString("output.json")); def->set_default_value(new ConfigOptionString("output.json"));
+3
View File
@@ -314,6 +314,9 @@ extern unsigned get_current_pid();
std::string per_user_temp_id(); std::string per_user_temp_id();
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged. // Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
std::string per_user_temp_dir(const std::string &base, const std::string &user_id); std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
// Completes a relative command line input path against the current working directory. Absolute
// paths and custom open protocol URLs are returned unchanged.
std::string resolve_cli_input_path(const std::string &path);
// BBS: backup & restore // BBS: backup & restore
std::string get_process_name(int pid); std::string get_process_name(int pid);
+13
View File
@@ -1339,6 +1339,19 @@ std::string per_user_temp_dir(const std::string &base, const std::string &user_i
return base + "/orcaslicer_" + user_id; return base + "/orcaslicer_" + user_id;
} }
std::string resolve_cli_input_path(const std::string &path)
{
const boost::filesystem::path input(path);
if (path.empty() || is_supported_open_protocol(path) || input.is_absolute())
return path;
boost::system::error_code ec;
const boost::filesystem::path resolved = boost::filesystem::system_complete(input, ec);
if (ec)
return path;
return resolved.lexically_normal().make_preferred().string();
}
// BBS: backup & restore // BBS: backup & restore
std::string get_process_name(int pid) std::string get_process_name(int pid)
{ {
+3 -8
View File
@@ -2098,12 +2098,6 @@ void GLCanvas3D::render(bool only_init)
_render_selection_center(); _render_selection_center();
#endif // ENABLE_RENDER_SELECTION_CENTER #endif // ENABLE_RENDER_SELECTION_CENTER
// we need to set the mouse's scene position here because the depth buffer
// could be invalidated by the following gizmo render methods
// this position is used later into on_mouse() to drag the objects
if (m_picking_enabled)
m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast<coord_t>());
// sidebar hints need to be rendered before the gizmos because the depth buffer // sidebar hints need to be rendered before the gizmos because the depth buffer
// could be invalidated by the following gizmo render methods // could be invalidated by the following gizmo render methods
_render_selection_sidebar_hints(); _render_selection_sidebar_hints();
@@ -4491,12 +4485,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box(); BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box();
volume_bbox.offset(1.0); volume_bbox.offset(1.0);
const bool is_cut_connector_selected = m_selection.is_any_connector(); const bool is_cut_connector_selected = m_selection.is_any_connector();
if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position) && !is_cut_connector_selected) { const Vec3d scene_position = _mouse_to_3d(pos);
if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(scene_position) && !is_cut_connector_selected) {
m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None; m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None;
// The dragging operation is initiated. // The dragging operation is initiated.
m_mouse.drag.move_volume_idx = volume_idx; m_mouse.drag.move_volume_idx = volume_idx;
m_selection.setup_cache(); m_selection.setup_cache();
m_mouse.drag.start_position_3D = m_mouse.scene_position; m_mouse.drag.start_position_3D = scene_position;
m_sequential_print_clearance_first_displacement = true; m_sequential_print_clearance_first_displacement = true;
m_moving = true; m_moving = true;
-1
View File
@@ -337,7 +337,6 @@ class GLCanvas3D
bool dragging{ false }; bool dragging{ false };
Vec2d position{ DBL_MAX, DBL_MAX }; Vec2d position{ DBL_MAX, DBL_MAX };
Vec3d scene_position{ DBL_MAX, DBL_MAX, DBL_MAX };
bool ignore_left_up{ false }; bool ignore_left_up{ false };
Drag drag; Drag drag;
bool ignore_right_up; bool ignore_right_up;
-3
View File
@@ -1037,9 +1037,6 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style);
section.mixed_tabs->SetFont(Label::Body_14); section.mixed_tabs->SetFont(Label::Body_14);
section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); section.mixed_tabs->SetBackgroundColour(GetBackgroundColour());
// The mixed tabs carry full swatch compositions: give them a touch more room than the
// filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem).
section.mixed_tabs->SetItemSpace(FromDIP(3));
page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2));
section.mixed_tabs->Hide(); section.mixed_tabs->Hide();
} }
+6 -3
View File
@@ -311,8 +311,11 @@ void Button::render(wxDC& dc)
} }
} }
auto szContent = textSize; auto szContent = textSize;
// Whether the measured content reserved the text/icon gap. macOS measures an empty label
// as 0-high, so the gap is skipped there; the dot must not advance past it in that case.
const bool gap_reserved = szContent.y > 0;
if (icon.bmp().IsOk()) { if (icon.bmp().IsOk()) {
if (szContent.y > 0) { if (gap_reserved) {
//BBS norrow size between text and icon //BBS norrow size between text and icon
if (vertical) if (vertical)
szContent.y += spacing; szContent.y += spacing;
@@ -357,10 +360,10 @@ void Button::render(wxDC& dc)
dc.DrawBitmap(icon.bmp(), pt); dc.DrawBitmap(icon.bmp(), pt);
//BBS norrow size between text and icon //BBS norrow size between text and icon
if (vertical) { if (vertical) {
pt.y += szIcon.y + spacing; pt.y += szIcon.y + (gap_reserved ? spacing : 0);
pt.x = rcContent.x; pt.x = rcContent.x;
} else { } else {
pt.x += szIcon.x + spacing; pt.x += szIcon.x + (gap_reserved ? spacing : 0);
pt.y = rcContent.y; pt.y = rcContent.y;
} }
} }
+7 -16
View File
@@ -99,7 +99,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli
btns.push_back(btn); btns.push_back(btn);
if (btns.size() > 1) if (btns.size() > 1)
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0}); sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space); sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL);
sizer->AddStretchSpacer(1); sizer->AddStretchSpacer(1);
relayout(); relayout();
return btns.size() - 1; return btns.size() - 1;
@@ -256,12 +256,12 @@ void TabCtrl::relayout()
int item = sel + 1; int item = sel + 1;
int first = 0; int first = 0;
for (int i = 0; i < item; ++i) for (int i = 0; i < item; ++i)
offset += btns[i]->GetMinSize().x + item_space * 2; offset += btns[i]->GetMinSize().x;
if (item < btns.size()) if (item < btns.size())
offset += btns[item]->GetMinSize().x + item_space * 2; offset += btns[item]->GetMinSize().x;
int width = GetSize().x; int width = GetSize().x;
for (int i = 0; i < btns.size(); ++i) { for (int i = 0; i < btns.size(); ++i) {
auto size = btns[i]->GetMinSize().x + item_space * 2; auto size = btns[i]->GetMinSize().x;
if (i < sel && offset > width) { if (i < sel && offset > width) {
sizer->Show(i * 2 + 1, false); sizer->Show(i * 2 + 1, false);
sizer->Show(i * 2 + 2, false); sizer->Show(i * 2 + 2, false);
@@ -284,26 +284,17 @@ void TabCtrl::relayout()
if (item >= btns.size()) if (item >= btns.size())
--item; --item;
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE // Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8; int b = GetSize().x - offset - 10 - (item + 1 - first) * 16;
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0}); sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
Layout(); Layout();
} }
void TabCtrl::SetItemSpace(int space)
{
if (space < 0 || space == item_space)
return;
item_space = space;
relayout();
Refresh();
}
int TabCtrl::GetFullSize() const int TabCtrl::GetFullSize() const
{ {
// Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing. // Mirrors relayout(): a 10px leading spacer plus every button's min width.
int width = 10; int width = 10;
for (const Button* btn : btns) for (const Button* btn : btns)
width += btn->GetMinSize().x + item_space * 2; width += btn->GetMinSize().x;
return width; return width;
} }
-5
View File
@@ -14,7 +14,6 @@ class TabCtrl : public StaticBox
int sel = -1; int sel = -1;
wxFont bold; wxFont bold;
int item_space = 2; // space around each button, both sides (SetItemSpace)
public: public:
TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0); TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
@@ -64,10 +63,6 @@ public:
int GetNextVisible(int item) const; int GetNextVisible(int item) const;
bool IsVisible(unsigned int item) const; bool IsVisible(unsigned int item) const;
// Extra space around each tab button (in px on both sides). Defaults to the control-wide
// standard; call before appending items so every button picks it up.
void SetItemSpace(int space);
int GetFullSize() const; int GetFullSize() const;
private: private:
+68
View File
@@ -15,6 +15,7 @@
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/SVG.hpp" #include "libslic3r/SVG.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
@@ -676,6 +677,73 @@ TEST_CASE("Ironing follows the solid infill rotation template", "[Fill]")
REQUIRE(compared > int(ironing.size()) / 2); REQUIRE(compared > int(ironing.size()) / 2);
} }
namespace {
PrintRegionConfig ironing_config(IroningType type,
int top_surface_filament_id = 1,
int top_shell_layers = 3,
int bottom_shell_layers = 1)
{
PrintRegionConfig cfg;
cfg.ironing_type.value = type;
cfg.top_surface_filament_id.value = top_surface_filament_id;
cfg.top_shell_layers.value = top_shell_layers;
cfg.bottom_shell_layers.value = bottom_shell_layers;
cfg.outer_wall_filament_id.value = 1;
cfg.wall_loops.value = 2;
return cfg;
}
} // namespace
TEST_CASE("Ironing an all-solid region uses the top surface filament on every layer", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::AllSolid, /*top_surface_filament_id=*/2);
const bool is_topmost_layer = GENERATE(false, true);
CAPTURE(is_topmost_layer);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, is_topmost_layer) == 2);
}
TEST_CASE("Ironing top surfaces uses the top surface filament when the region has top shells", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::TopSurfaces,
/*top_surface_filament_id=*/3,
/*top_shell_layers=*/2);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == 3);
}
TEST_CASE("Ironing top surfaces without top shells needs spiral mode and more than one bottom shell", "[Fill]")
{
const PrintRegionConfig one_bottom_shell = ironing_config(IroningType::TopSurfaces,
/*top_surface_filament_id=*/1,
/*top_shell_layers=*/0,
/*bottom_shell_layers=*/1);
const PrintRegionConfig two_bottom_shells = ironing_config(IroningType::TopSurfaces,
/*top_surface_filament_id=*/1,
/*top_shell_layers=*/0,
/*bottom_shell_layers=*/2);
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == 1);
REQUIRE(Layer::choose_ironing_extruder(one_bottom_shell, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == -1);
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
}
TEST_CASE("Ironing the topmost surface only applies to the topmost layer", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::TopmostOnly, /*top_surface_filament_id=*/4);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/true) == 4);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
}
TEST_CASE("A region with ironing turned off is never ironed", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::NoIroning);
const bool spiral_mode = GENERATE(false, true);
CAPTURE(spiral_mode);
REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1);
}
TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]") TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]")
{ {
auto angles_for = [](int direction) { auto angles_for = [](int direction) {
+55
View File
@@ -15,6 +15,8 @@
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <sstream>
using namespace Slic3r; using namespace Slic3r;
SCENARIO("Generic config validation performs as expected.", "[Config]") { SCENARIO("Generic config validation performs as expected.", "[Config]") {
@@ -488,6 +490,59 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs); CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
} }
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
DynamicPrintConfig config;
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("wall_loops", new ConfigOptionInt(3));
config.set_key_value("filament_type", new ConfigOptionStrings({ "PLA", "PETG" }));
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28\nG1 Z5"));
ScopedTemporaryFile tmp(".json");
config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0");
std::string file_contents;
{
boost::nowide::ifstream ifs(tmp.string());
file_contents.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
// The file format: one tab per nesting level and a trailing newline.
REQUIRE_FALSE(file_contents.empty());
CHECK(file_contents.rfind("{\n\t\"", 0) == 0);
CHECK(file_contents.back() == '\n');
std::ostringstream strict, replaced;
config.save_to_json(strict, "test_preset", "User", "1.0.0.0");
config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true);
CHECK(strict.str() == file_contents);
CHECK(replaced.str() == file_contents);
CHECK(nlohmann::json::parse(strict.str())["machine_start_gcode"] == "G28\nG1 Z5");
}
TEST_CASE("save_to_json replaces invalid UTF-8 in a stream only when asked", "[Config]") {
DynamicPrintConfig config;
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
std::ostringstream strict, replaced;
CHECK_THROWS_AS(config.save_to_json(strict, "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
REQUIRE_NOTHROW(config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true));
CHECK(nlohmann::json::parse(replaced.str())["machine_start_gcode"] == "G28 ; \xEF\xBF\xBD");
}
TEST_CASE("save_to_json leaves an existing file untouched when the config cannot be serialized", "[Config]") {
DynamicPrintConfig config;
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
ScopedTemporaryFile tmp(".json");
{
boost::nowide::ofstream ofs(tmp.string());
ofs << "previous";
}
CHECK_THROWS_AS(config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
boost::nowide::ifstream ifs(tmp.string());
const std::string contents((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
CHECK(contents == "previous");
}
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") { TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
const std::vector<std::string> refs = { const std::vector<std::string> refs = {
"master_plugin;;header-stamp", "master_plugin;;header-stamp",
@@ -987,6 +987,193 @@ TEST_CASE("Resolution terminates when no vendor manifest exists", "[Preset][Bund
CHECK(error == "Preset was not found in the loaded bundle"); CHECK(error == "Preset was not found in the loaded bundle");
} }
TEST_CASE("Manifest-backed resolution reuses the vendor tree it already loaded", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir dir;
const fs::path process_dir = dir.path() / "Acme" / "process";
fs::create_directories(process_dir);
std::ofstream((dir.path() / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme","process_list":[)"
<< R"({"name":"fdm_process_common","sub_path":"process/base.json"},)"
<< R"({"name":"Acme First","sub_path":"process/first.json"},)"
<< R"({"name":"Acme Second","sub_path":"process/second.json"}]})";
auto write_base = [&](double travel_speed) {
std::ofstream((process_dir / "base.json").string())
<< R"({"type":"process","name":"fdm_process_common","from":"system",)"
<< R"("instantiation":"false","travel_speed":[")" << travel_speed << R"("]})";
};
auto write_child = [&](const std::string &file, const std::string &name) {
std::ofstream((process_dir / file).string())
<< R"({"type":"process","name":")" << name << R"(","from":"system",)"
<< R"("instantiation":"true","inherits":"fdm_process_common"})";
};
write_base(111.0);
write_child("first.json", "Acme First");
write_child("second.json", "Acme Second");
auto travel_speed = [&](PresetBundle &bundle, const std::string &file) {
DynamicPrintConfig raw;
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common";
std::string error;
REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, (process_dir / file).string(),
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
return raw.option<ConfigOptionFloats>("travel_speed")->values.front();
};
PresetBundle bundle;
CHECK_THAT(travel_speed(bundle, "first.json"), Catch::Matchers::WithinAbs(111.0, 1e-6));
// Only a reload would see this change.
write_base(222.0);
CHECK_THAT(travel_speed(bundle, "second.json"), Catch::Matchers::WithinAbs(111.0, 1e-6));
PresetBundle fresh;
CHECK_THAT(travel_speed(fresh, "second.json"), Catch::Matchers::WithinAbs(222.0, 1e-6));
}
TEST_CASE("Manifest-backed resolution does not keep a vendor tree that failed to load", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir dir;
const fs::path child_file = dir.path() / "Acme" / "process" / "child.json";
auto write_manifest = [&](const std::string &leading_entry) {
std::ofstream((dir.path() / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme","process_list":[)" << leading_entry
<< R"({"name":"Acme Process","sub_path":"process/child.json"}]})";
};
write_manifest("123,");
fs::create_directories(child_file.parent_path());
std::ofstream(child_file.string())
<< R"({"type":"process","name":"Acme Process","from":"system",)"
<< R"("instantiation":"true","layer_height":"0.2"})";
PresetBundle bundle;
auto resolve = [&](std::string &error) {
DynamicPrintConfig raw;
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common";
return bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(),
ForwardCompatibilitySubstitutionRule::EnableSilent, error);
};
std::string error;
CHECK_FALSE(resolve(error));
CHECK_FALSE(error.empty());
write_manifest("");
error.clear();
CHECK(resolve(error));
CHECK(error.empty());
}
TEST_CASE("Manifest-backed resolution reuses the library base for type-probed files", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir dir;
const fs::path library_pet = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament" / "pet.json";
const fs::path filament_dir = dir.path() / "Acme" / "filament";
std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string())
<< R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)"
<< R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"}]})";
fs::create_directories(library_pet.parent_path());
auto write_library_pet = [&](double density) {
std::ofstream(library_pet.string())
<< R"({"type":"filament","name":"fdm_filament_pet","from":"system",)"
<< R"("filament_id":"GFL99","instantiation":"false",)"
<< R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})";
};
write_library_pet(1.27);
std::ofstream((dir.path() / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme","filament_list":[)"
<< R"({"name":"Acme PETG","sub_path":"filament/petg.json","filament_id":"GFA00"},)"
<< R"({"name":"Acme PETG Matte","sub_path":"filament/petg_matte.json","filament_id":"GFA01"}]})";
fs::create_directories(filament_dir);
auto write_child = [&](const std::string &file, const std::string &name, const std::string &filament_id) {
std::ofstream((filament_dir / file).string())
<< R"({"type":"filament","name":")" << name << R"(","from":"system",)"
<< R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})";
};
write_child("petg.json", "Acme PETG", "GFA00");
write_child("petg_matte.json", "Acme PETG Matte", "GFA01");
auto density = [](const DynamicPrintConfig &config) {
return config.option<ConfigOptionFloats>("filament_density")->values.front();
};
PresetBundle bundle;
DynamicPrintConfig first;
first.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet";
std::string error;
REQUIRE(bundle.resolve_preset_config(first, Preset::TYPE_FILAMENT, (filament_dir / "petg.json").string(),
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(density(first), Catch::Matchers::WithinAbs(1.27, 1e-6));
// Only a reload would see this change.
write_library_pet(1.5);
DynamicPrintConfig second;
Preset::Type type = Preset::TYPE_INVALID;
REQUIRE(bundle.resolve_preset_config_type(second, type, (filament_dir / "petg_matte.json").string(),
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK(type == Preset::TYPE_FILAMENT);
CHECK_THAT(density(second), Catch::Matchers::WithinAbs(1.27, 1e-6));
}
TEST_CASE("Manifest-backed resolution shares the library between vendors under one root", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir dir;
const fs::path library_dir = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament";
std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string())
<< R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)"
<< R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"},)"
<< R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json","filament_id":"GFL98"}]})";
fs::create_directories(library_dir);
auto write_library_pet = [&](double density) {
std::ofstream((library_dir / "pet.json").string())
<< R"({"type":"filament","name":"fdm_filament_pet","from":"system",)"
<< R"("filament_id":"GFL99","instantiation":"false",)"
<< R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})";
};
write_library_pet(1.27);
std::ofstream((library_dir / "generic_petg.json").string())
<< R"({"type":"filament","name":"Generic PETG","from":"system",)"
<< R"("filament_id":"GFL98","instantiation":"true","inherits":"fdm_filament_pet"})";
auto write_vendor = [&](const std::string &vendor, const std::string &filament_id) {
const fs::path filament_dir = dir.path() / vendor / "filament";
fs::create_directories(filament_dir);
std::ofstream((dir.path() / (vendor + ".json")).string())
<< R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)"
<< R"({"name":")" << vendor << R"( PETG","sub_path":"filament/petg.json","filament_id":")" << filament_id << R"("}]})";
std::ofstream((filament_dir / "petg.json").string())
<< R"({"type":"filament","name":")" << vendor << R"( PETG","from":"system",)"
<< R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})";
return filament_dir / "petg.json";
};
const fs::path acme_petg = write_vendor("Acme", "GFA00");
const fs::path beta_petg = write_vendor("Beta", "GFB00");
auto density = [&](PresetBundle &bundle, const fs::path &file) {
DynamicPrintConfig raw;
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet";
std::string error;
REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_FILAMENT, file.string(),
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
return raw.option<ConfigOptionFloats>("filament_density")->values.front();
};
PresetBundle bundle;
CHECK_THAT(density(bundle, acme_petg), Catch::Matchers::WithinAbs(1.27, 1e-6));
// Only a reload would see this change.
write_library_pet(1.5);
CHECK_THAT(density(bundle, beta_petg), Catch::Matchers::WithinAbs(1.27, 1e-6));
CHECK_THAT(density(bundle, library_dir / "generic_petg.json"), Catch::Matchers::WithinAbs(1.27, 1e-6));
PresetBundle fresh;
CHECK_THAT(density(fresh, beta_petg), Catch::Matchers::WithinAbs(1.5, 1e-6));
}
// Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic // Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic
// library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible // library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible
// with that printer and the plater combo box lists the shared alias twice. // with that printer and the plater combo box lists the shared alias twice.
+64
View File
@@ -4,6 +4,8 @@
#include "test_utils.hpp" #include "test_utils.hpp"
#include <boost/filesystem.hpp>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <fstream> #include <fstream>
@@ -88,3 +90,65 @@ TEST_CASE("copy_file reports the OS error when the destination cannot be written
REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; })); REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; }));
#endif // _WIN32 #endif // _WIN32
} }
TEST_CASE("A resolved input path still names the same file after the working directory changes", "[utils]") {
ScopedTemporaryFile model(".3mf");
{ std::ofstream out(model.string()); out << "3mf"; }
const std::string name = model.path().filename().string();
// Resolve the bare name from the directory holding the file, then move away from it. The guard
// restores the directory the test started in, wherever this leaves it.
ScopedWorkingDirectory cwd(model.path().parent_path());
const std::string resolved = resolve_cli_input_path(name);
boost::filesystem::current_path(boost::filesystem::path(TEST_DATA_DIR));
REQUIRE(boost::filesystem::exists(resolved));
REQUIRE(boost::filesystem::equivalent(resolved, model.path()));
// Control: the bare name finds nothing from here, so resolving it this late would have failed.
REQUIRE_FALSE(boost::filesystem::exists(name));
}
TEST_CASE("resolve_cli_input_path completes a relative path against the working directory", "[utils]") {
ScopedWorkingDirectory cwd(boost::filesystem::temp_directory_path());
// Read back rather than reusing temp_directory_path(): changing to it resolves any symlink.
const boost::filesystem::path here = boost::filesystem::current_path();
SECTION("a bare name") {
REQUIRE(resolve_cli_input_path("model.3mf") == (here / "model.3mf").make_preferred().string());
}
SECTION("a ./ prefix is dropped") {
REQUIRE(resolve_cli_input_path("./model.3mf") == (here / "model.3mf").make_preferred().string());
}
SECTION("a ../ traversal is collapsed") {
REQUIRE(resolve_cli_input_path("../model.3mf") == (here.parent_path() / "model.3mf").make_preferred().string());
}
}
TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed unchanged", "[utils]") {
SECTION("an absolute path") {
const boost::filesystem::path absolute = (boost::filesystem::temp_directory_path() / "model.3mf").make_preferred();
REQUIRE(resolve_cli_input_path(absolute.string()) == absolute.string());
}
#ifdef _WIN32
// Every absolute form Windows accepts opens today, so each must come back byte for byte:
// normalizing them would rewrite the forward slashes and rebuild the \\?\ and UNC prefixes.
SECTION("an absolute Windows path of any form") {
for (const std::string absolute : {R"(C:\models\model.3mf)",
R"(C:/models/model.3mf)",
R"(\\server\share\model.3mf)",
R"(\\?\C:\models\model.3mf)"})
REQUIRE(resolve_cli_input_path(absolute) == absolute);
}
#endif
// These are downloaded rather than opened, and completing one would produce a path, not a URL.
SECTION("a custom open protocol URL") {
for (const std::string url : {"orcaslicer://open/?file=https://example.com/model.3mf",
"prusaslicer://open/?file=https://example.com/model.3mf",
"bambustudio://open/?file=https://example.com/model.3mf",
"cura://open/?file=https://example.com/model.3mf"})
REQUIRE(resolve_cli_input_path(url) == url);
}
SECTION("an empty argument") {
REQUIRE(resolve_cli_input_path("").empty());
}
}
+18
View File
@@ -176,4 +176,22 @@ inline void write_debug_stream([[maybe_unused]] const std::string &name, [[maybe
#endif #endif
} }
// Changes the working directory and restores the previous one on scope exit, including when an
// assertion throws. It is process wide state shared with every other test.
class ScopedWorkingDirectory
{
public:
explicit ScopedWorkingDirectory(const boost::filesystem::path &dir)
: m_previous(boost::filesystem::current_path())
{
boost::filesystem::current_path(dir);
}
~ScopedWorkingDirectory() { boost::system::error_code ec; boost::filesystem::current_path(m_previous, ec); }
ScopedWorkingDirectory(const ScopedWorkingDirectory &) = delete;
ScopedWorkingDirectory &operator=(const ScopedWorkingDirectory &) = delete;
private:
boost::filesystem::path m_previous;
};
#endif // SLIC3R_TEST_UTILS #endif // SLIC3R_TEST_UTILS