mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173706750d | ||
|
|
0956b4d8fe | ||
|
|
5514559feb | ||
|
|
d5cf1502c4 | ||
|
|
37e2b6c928 | ||
|
|
efc9f253ee | ||
|
|
292cf0095e | ||
|
|
5c635d5e50 | ||
|
|
5496883493 | ||
|
|
31eb8a2bd1 | ||
|
|
70247ad298 | ||
|
|
d4840901fc | ||
|
|
5f01f21661 | ||
|
|
ecbe1b1b90 | ||
|
|
ffb4f192c1 | ||
|
|
4373bc3697 | ||
|
|
ffb2946b20 | ||
|
|
e89a05b1e4 | ||
|
|
02a8011b97 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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,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": [
|
||||||
|
|||||||
+5
-5
@@ -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"
|
||||||
|
|||||||
+8
-8
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user