mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-17 05:52:39 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c28701894 | ||
|
|
0956b4d8fe | ||
|
|
5514559feb | ||
|
|
d5cf1502c4 | ||
|
|
37e2b6c928 | ||
|
|
efc9f253ee | ||
|
|
292cf0095e | ||
|
|
5c635d5e50 | ||
|
|
5496883493 | ||
|
|
31eb8a2bd1 | ||
|
|
70247ad298 | ||
|
|
d4840901fc | ||
|
|
5f01f21661 | ||
|
|
ecbe1b1b90 | ||
|
|
ffb4f192c1 | ||
|
|
4373bc3697 |
@@ -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
|
||||||
+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"
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.12"
|
||||||
|
#
|
||||||
|
# [tool.orcaslicer.plugin]
|
||||||
|
# name = "Dock Panel Demo"
|
||||||
|
# description = "Opens a dockable panel beside the 3D view that lists the objects on the plate."
|
||||||
|
# author = "OrcaSlicer"
|
||||||
|
# version = "0.0.1"
|
||||||
|
# ///
|
||||||
|
"""Dock Panel Demo -- orca.host.ui.create_dock_panel().
|
||||||
|
|
||||||
|
Run it from the Plugins dialog. It opens an HTML panel docked on the right of the 3D view, in the
|
||||||
|
same dock area as the sidebar. Drag its caption to dock it on another side (or float it, where the
|
||||||
|
platform allows), hide it from the page and run the plugin again to bring it back, or close it with
|
||||||
|
its close button or from the page.
|
||||||
|
|
||||||
|
page --orca.postMessage({command: 'refresh'})--> plugin.on_message()
|
||||||
|
page --orca.postMessage({command: 'hide'})--> plugin.on_message() -> panel.hide()
|
||||||
|
page --orca.close()--> panel closes, plugin.on_close()
|
||||||
|
plugin --panel.post({command: 'objects', ...})--> page (orca.onMessage)
|
||||||
|
"""
|
||||||
|
import orca
|
||||||
|
|
||||||
|
PAGE = """
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 12px; font-size: 13px; }
|
||||||
|
h3 { margin: 0 0 4px; font-size: 14px; }
|
||||||
|
.note { margin: 0 0 12px; color: var(--orca-muted); font-size: 12px; }
|
||||||
|
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||||
|
.actions button.quiet { background: transparent; color: var(--orca-fg); border-color: var(--orca-border); }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
td.count { text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
#status { margin-top: 10px; color: var(--orca-muted); font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h3>Objects on the plate</h3>
|
||||||
|
<p class="note">Docked beside the 3D view. Drag the caption to move it.</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="refresh">Refresh</button>
|
||||||
|
<button type="button" id="hide" class="quiet">Hide</button>
|
||||||
|
<button type="button" id="close" class="quiet">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Name</th><th>Parts</th><th>Copies</th></tr></thead>
|
||||||
|
<tbody id="rows"></tbody>
|
||||||
|
</table>
|
||||||
|
<p id="status">Waiting for the plugin...</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function text(value) {
|
||||||
|
var span = document.createElement("span");
|
||||||
|
span.textContent = value;
|
||||||
|
return span.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(message) {
|
||||||
|
var rows = document.getElementById("rows");
|
||||||
|
var status = document.getElementById("status");
|
||||||
|
if (message.error) {
|
||||||
|
rows.innerHTML = "";
|
||||||
|
status.textContent = message.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.innerHTML = message.objects.map(function (object) {
|
||||||
|
return "<tr><td>" + text(object.name) + "</td><td class=\\"count\\">" + object.volumes +
|
||||||
|
"</td><td class=\\"count\\">" + object.instances + "</td></tr>";
|
||||||
|
}).join("");
|
||||||
|
status.textContent = message.objects.length + " object(s), refreshed " + new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
orca.onMessage(function (message) {
|
||||||
|
if (message && message.command === "objects")
|
||||||
|
render(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("refresh").addEventListener("click", function () {
|
||||||
|
orca.postMessage({ command: "refresh" });
|
||||||
|
});
|
||||||
|
document.getElementById("hide").addEventListener("click", function () {
|
||||||
|
orca.postMessage({ command: "hide" });
|
||||||
|
});
|
||||||
|
document.getElementById("close").addEventListener("click", function () {
|
||||||
|
orca.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
orca.postMessage({ command: "refresh" });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def plate_objects():
|
||||||
|
try:
|
||||||
|
model = orca.host.model()
|
||||||
|
except RuntimeError as error:
|
||||||
|
return {"command": "objects", "error": str(error)}
|
||||||
|
return {
|
||||||
|
"command": "objects",
|
||||||
|
"objects": [
|
||||||
|
{"name": obj.name or "(unnamed)", "volumes": obj.volume_count(), "instances": obj.instance_count()}
|
||||||
|
for obj in model.objects()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DockPanelDemo(orca.script.ScriptPluginCapabilityBase):
|
||||||
|
panel = None
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return "Dock Panel Demo"
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
# The capability instance lives as long as the plugin, so a second run finds the open panel.
|
||||||
|
if self.panel is not None and self.panel.is_open():
|
||||||
|
self.panel.show()
|
||||||
|
return orca.ExecutionResult.success("Dock Panel Demo is already open.")
|
||||||
|
self.panel = orca.host.ui.create_dock_panel(
|
||||||
|
html=PAGE,
|
||||||
|
title="Dock Panel Demo",
|
||||||
|
dock="right",
|
||||||
|
width=320,
|
||||||
|
height=480,
|
||||||
|
on_message=self.on_message,
|
||||||
|
on_close=self.on_close,
|
||||||
|
)
|
||||||
|
return orca.ExecutionResult.success("Dock Panel Demo opened.")
|
||||||
|
|
||||||
|
# Called on the UI thread when the page posts.
|
||||||
|
def on_message(self, message):
|
||||||
|
command = (message or {}).get("command")
|
||||||
|
if command == "refresh":
|
||||||
|
self.panel.post(plate_objects())
|
||||||
|
elif command == "hide":
|
||||||
|
self.panel.hide()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self.panel = None
|
||||||
|
|
||||||
|
|
||||||
|
@orca.plugin
|
||||||
|
class DockPanelDemoPlugin(orca.base):
|
||||||
|
def register_capabilities(self):
|
||||||
|
orca.register_capability(DockPanelDemo)
|
||||||
+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))
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
GUI/PluginProgressDialog.hpp
|
GUI/PluginProgressDialog.hpp
|
||||||
GUI/PluginWebDialog.cpp
|
GUI/PluginWebDialog.cpp
|
||||||
GUI/PluginWebDialog.hpp
|
GUI/PluginWebDialog.hpp
|
||||||
|
GUI/PluginDockPanel.cpp
|
||||||
|
GUI/PluginDockPanel.hpp
|
||||||
GUI/DragCanvas.cpp
|
GUI/DragCanvas.cpp
|
||||||
GUI/DragCanvas.hpp
|
GUI/DragCanvas.hpp
|
||||||
GUI/EditGCodeDialog.cpp
|
GUI/EditGCodeDialog.cpp
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
#ifdef __WXGTK__
|
#ifdef __WXGTK__
|
||||||
#include "LinuxDisplayBackend.hpp"
|
#include "LinuxDisplayBackend.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
#include "PluginDockPanel.hpp"
|
||||||
#include "GUI_Utils.hpp"
|
#include "GUI_Utils.hpp"
|
||||||
#include "GUI_Factories.hpp"
|
#include "GUI_Factories.hpp"
|
||||||
#include "wxExtensions.hpp"
|
#include "wxExtensions.hpp"
|
||||||
@@ -6720,6 +6721,14 @@ struct Plater::priv
|
|||||||
|
|
||||||
// GUI elements
|
// GUI elements
|
||||||
AuiMgr m_aui_mgr;
|
AuiMgr m_aui_mgr;
|
||||||
|
// Live plugin panes. `on_close` runs when the user closes one from its close button; `shown` is
|
||||||
|
// what the plugin asked for.
|
||||||
|
struct PluginPane
|
||||||
|
{
|
||||||
|
std::function<void()> on_close;
|
||||||
|
bool shown{true};
|
||||||
|
};
|
||||||
|
std::map<wxWindow*, PluginPane> m_plugin_panes;
|
||||||
wxString m_default_window_layout;
|
wxString m_default_window_layout;
|
||||||
wxPanel* current_panel{ nullptr };
|
wxPanel* current_panel{ nullptr };
|
||||||
std::vector<wxPanel*> panels;
|
std::vector<wxPanel*> panels;
|
||||||
@@ -6891,6 +6900,11 @@ struct Plater::priv
|
|||||||
void update_sidebar(bool force_update = false);
|
void update_sidebar(bool force_update = false);
|
||||||
void reset_window_layout();
|
void reset_window_layout();
|
||||||
Sidebar::DockingState get_sidebar_docking_state();
|
Sidebar::DockingState get_sidebar_docking_state();
|
||||||
|
void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close);
|
||||||
|
void remove_plugin_pane(wxWindow* window);
|
||||||
|
void show_plugin_pane(wxWindow* window, bool show);
|
||||||
|
bool plugin_pane_visible(const PluginPane& plugin_pane, const wxAuiPaneInfo& pane) const;
|
||||||
|
|
||||||
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
||||||
|
|
||||||
@@ -7471,6 +7485,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
|||||||
panel_3d->SetSizer(panel_sizer);
|
panel_3d->SetSizer(panel_sizer);
|
||||||
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
|
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
|
||||||
|
|
||||||
|
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& evt) {
|
||||||
|
const wxAuiPaneInfo* pane = evt.GetPane();
|
||||||
|
auto it = pane != nullptr ? m_plugin_panes.find(pane->window) : m_plugin_panes.end();
|
||||||
|
if (it != m_plugin_panes.end()) {
|
||||||
|
const std::function<void()> on_close = std::move(it->second.on_close);
|
||||||
|
m_plugin_panes.erase(it);
|
||||||
|
if (on_close)
|
||||||
|
on_close();
|
||||||
|
}
|
||||||
|
evt.Skip();
|
||||||
|
});
|
||||||
|
|
||||||
m_default_window_layout = m_aui_mgr.SavePerspective();
|
m_default_window_layout = m_aui_mgr.SavePerspective();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -8141,6 +8167,14 @@ void Plater::priv::update_sidebar(bool force_update) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const auto& [window, plugin_pane] : m_plugin_panes) {
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (pane.IsOk() && pane.IsShown() != plugin_pane_visible(plugin_pane, pane)) {
|
||||||
|
pane.Show(!pane.IsShown());
|
||||||
|
needs_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (needs_update) {
|
if (needs_update) {
|
||||||
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
||||||
m_aui_mgr.Update();
|
m_aui_mgr.Update();
|
||||||
@@ -8150,10 +8184,95 @@ void Plater::priv::update_sidebar(bool force_update) {
|
|||||||
void Plater::priv::reset_window_layout()
|
void Plater::priv::reset_window_layout()
|
||||||
{
|
{
|
||||||
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
|
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
|
||||||
|
// Loading a layout hides every pane it does not list, and the default layout lists no plugin panes.
|
||||||
|
for (const auto& [window, plugin_pane] : m_plugin_panes)
|
||||||
|
if (wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); pane.IsOk())
|
||||||
|
pane.Show(plugin_pane.shown);
|
||||||
sidebar_layout.is_collapsed = false;
|
sidebar_layout.is_collapsed = false;
|
||||||
update_sidebar(true);
|
update_sidebar(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Plater::priv::plugin_pane_visible(const PluginPane& plugin_pane, const wxAuiPaneInfo& pane) const
|
||||||
|
{
|
||||||
|
// A floating pane is a top-level window, so it does not hide with the Plater on other tabs.
|
||||||
|
return plugin_pane.shown && (!pane.IsFloating() || sidebar_layout.show);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close)
|
||||||
|
{
|
||||||
|
const wxString base_name = wxString::FromUTF8(name);
|
||||||
|
wxString unique_name = base_name;
|
||||||
|
for (int i = 2; m_aui_mgr.GetPane(unique_name).IsOk(); ++i)
|
||||||
|
unique_name = base_name + wxString::Format("#%d", i);
|
||||||
|
|
||||||
|
// A restored layout below already holds pixels.
|
||||||
|
const wxSize pixels = q->FromDIP(size);
|
||||||
|
wxAuiPaneInfo info;
|
||||||
|
info.Name(unique_name).Caption(caption).BestSize(pixels).FloatingSize(pixels).DestroyOnClose(true);
|
||||||
|
if (dock == "left")
|
||||||
|
info.Left();
|
||||||
|
else if (dock == "bottom")
|
||||||
|
info.Bottom();
|
||||||
|
else
|
||||||
|
info.Right();
|
||||||
|
if (dock == "float")
|
||||||
|
info.Float();
|
||||||
|
|
||||||
|
// Put the pane back where it was the last time the window layout was saved with it open.
|
||||||
|
const std::string saved = plugin_pane_layout_entry(wxGetApp().app_config->get("window_layout"), unique_name.utf8_string());
|
||||||
|
if (!saved.empty()) {
|
||||||
|
m_aui_mgr.LoadPaneInfo(wxString::FromUTF8(saved), info);
|
||||||
|
info.Caption(caption).DestroyOnClose(true).Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floating is disabled on Wayland.
|
||||||
|
if ((m_aui_mgr.GetFlags() & wxAUI_MGR_ALLOW_FLOATING) == 0) {
|
||||||
|
info.Dock().Floatable(false);
|
||||||
|
if (info.dock_direction == wxAUI_DOCK_NONE)
|
||||||
|
info.Right();
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginPane& plugin_pane = m_plugin_panes[window] = PluginPane{std::move(on_close)};
|
||||||
|
info.Show(plugin_pane_visible(plugin_pane, info));
|
||||||
|
m_aui_mgr.AddPane(window, info);
|
||||||
|
|
||||||
|
// wxAUI does not record a dragged sash in best_size, so track the docked size like the sidebar
|
||||||
|
// does, for the saved layout.
|
||||||
|
window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) {
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) {
|
||||||
|
const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM;
|
||||||
|
pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(),
|
||||||
|
horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight());
|
||||||
|
}
|
||||||
|
evt.Skip();
|
||||||
|
});
|
||||||
|
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::remove_plugin_pane(wxWindow* window)
|
||||||
|
{
|
||||||
|
m_plugin_panes.erase(window);
|
||||||
|
if (m_aui_mgr.DetachPane(window))
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
window->Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::show_plugin_pane(wxWindow* window, bool show)
|
||||||
|
{
|
||||||
|
const auto it = m_plugin_panes.find(window);
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (it == m_plugin_panes.end() || !pane.IsOk())
|
||||||
|
return;
|
||||||
|
it->second.shown = show;
|
||||||
|
if (pane.IsShown() == plugin_pane_visible(it->second, pane))
|
||||||
|
return;
|
||||||
|
pane.Show(!pane.IsShown());
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
}
|
||||||
|
|
||||||
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
|
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
|
||||||
if (!sidebar_layout.is_enabled) {
|
if (!sidebar_layout.is_enabled) {
|
||||||
return Sidebar::None;
|
return Sidebar::None;
|
||||||
@@ -17710,6 +17829,14 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
|
|||||||
|
|
||||||
void Plater::reset_window_layout() { p->reset_window_layout(); }
|
void Plater::reset_window_layout() { p->reset_window_layout(); }
|
||||||
|
|
||||||
|
void Plater::add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close)
|
||||||
|
{
|
||||||
|
p->add_plugin_pane(window, name, caption, dock, size, std::move(on_close));
|
||||||
|
}
|
||||||
|
void Plater::remove_plugin_pane(wxWindow* window) { p->remove_plugin_pane(window); }
|
||||||
|
void Plater::show_plugin_pane(wxWindow* window, bool show) { p->show_plugin_pane(window, show); }
|
||||||
|
|
||||||
//BBS
|
//BBS
|
||||||
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
|
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
|
||||||
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
|
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
|
||||||
|
|||||||
@@ -475,6 +475,14 @@ public:
|
|||||||
|
|
||||||
void reset_window_layout();
|
void reset_window_layout();
|
||||||
|
|
||||||
|
// Plugin panes dock alongside the sidebar; `window` must be a child of the Plater. `dock` is
|
||||||
|
// "left", "right", "bottom" or "float", and `size` is in DIPs. A pane closed from its own close
|
||||||
|
// button is destroyed after on_close runs; remove_plugin_pane() destroys it without calling on_close.
|
||||||
|
void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close);
|
||||||
|
void remove_plugin_pane(wxWindow* window);
|
||||||
|
void show_plugin_pane(wxWindow* window, bool show);
|
||||||
|
|
||||||
// Called after the Preferences dialog is closed and the program settings are saved.
|
// Called after the Preferences dialog is closed and the program settings are saved.
|
||||||
// Update the UI based on the current preferences.
|
// Update the UI based on the current preferences.
|
||||||
void update_ui_from_settings();
|
void update_ui_from_settings();
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
#include "PluginDockPanel.hpp"
|
||||||
|
|
||||||
|
#include "GUI.hpp"
|
||||||
|
#include "GUI_App.hpp"
|
||||||
|
#include "Plater.hpp"
|
||||||
|
#include "PluginWebDialog.hpp"
|
||||||
|
#include "Widgets/WebView.hpp"
|
||||||
|
#include "Widgets/WebViewHostDialog.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/Utils.hpp>
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
|
#include <wx/sizer.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title)
|
||||||
|
{
|
||||||
|
std::string name = "plugin:" + plugin_key + ":" + title;
|
||||||
|
std::replace_if(name.begin(), name.end(), [](char c) { return c == '|' || c == ';' || c == '=' || c == '\\'; }, '_');
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string plugin_pane_layout_entry(const std::string& layout, const std::string& pane_name)
|
||||||
|
{
|
||||||
|
// Panes are separated by '|'; SavePerspective() escapes a '|' inside a caption as "\|".
|
||||||
|
const std::string prefix = "name=" + pane_name + ";";
|
||||||
|
size_t begin = 0;
|
||||||
|
for (size_t i = 0; i <= layout.size(); ++i) {
|
||||||
|
if (i < layout.size() && (layout[i] != '|' || (i > 0 && layout[i - 1] == '\\')))
|
||||||
|
continue;
|
||||||
|
if (layout.compare(begin, prefix.size(), prefix) == 0)
|
||||||
|
return layout.substr(begin, i - begin);
|
||||||
|
begin = i + 1;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginDockPanel::PluginDockPanel(wxWindow* parent,
|
||||||
|
const std::string& html,
|
||||||
|
MessageHandler on_message,
|
||||||
|
CloseHandler on_close,
|
||||||
|
CloseHandler on_destroyed)
|
||||||
|
: wxPanel(parent, wxID_ANY)
|
||||||
|
, m_html(html)
|
||||||
|
, m_on_message(std::move(on_message))
|
||||||
|
, m_on_close(std::move(on_close))
|
||||||
|
, m_on_destroyed(std::move(on_destroyed))
|
||||||
|
{
|
||||||
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
|
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
SetSizer(sizer);
|
||||||
|
|
||||||
|
const std::string bootstrap = (boost::filesystem::path(resources_dir()) / PluginWebDialog::BOOTSTRAP_PAGE).make_preferred().string();
|
||||||
|
m_browser = WebView::CreateWebView(this, wxString("file://") + from_u8(bootstrap));
|
||||||
|
if (m_browser == nullptr) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Could not create the web view for a plugin dock panel";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
||||||
|
m_browser->AddUserScript(PluginWebDialog::bridge_user_script());
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginDockPanel::on_bootstrap_event, this);
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginDockPanel::on_bootstrap_event, this);
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); });
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginDockPanel::on_script_message, this);
|
||||||
|
m_browser->Bind(EVT_WEBVIEW_RECREATED, &PluginDockPanel::on_webview_recreated, this);
|
||||||
|
sizer->Add(m_browser, 1, wxEXPAND);
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginDockPanel::~PluginDockPanel()
|
||||||
|
{
|
||||||
|
if (m_on_destroyed)
|
||||||
|
m_on_destroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::on_bootstrap_event(wxWebViewEvent& event)
|
||||||
|
{
|
||||||
|
if (!m_content_loaded) {
|
||||||
|
m_content_loaded = true;
|
||||||
|
m_browser->SetPage(wxString::FromUTF8(m_html), PluginWebDialog::content_base_url());
|
||||||
|
}
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::on_script_message(wxWebViewEvent& event)
|
||||||
|
{
|
||||||
|
const nlohmann::json payload = nlohmann::json::parse(event.GetString().utf8_string(), nullptr, false);
|
||||||
|
if (!payload.is_object() || payload.value("channel", std::string()) != "orca")
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::string kind = payload.value("kind", std::string());
|
||||||
|
if (kind == "message") {
|
||||||
|
if (m_on_message)
|
||||||
|
m_on_message(payload.contains("data") ? payload["data"] : nlohmann::json());
|
||||||
|
} else if (kind == "close") {
|
||||||
|
request_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::on_webview_recreated(wxCommandEvent&)
|
||||||
|
{
|
||||||
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
|
Refresh();
|
||||||
|
// Handled without Skip(), so WebView::RecreateAll() does not reload the plugin page.
|
||||||
|
WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::push_message(const nlohmann::json& data)
|
||||||
|
{
|
||||||
|
if (m_browser == nullptr || m_closing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
nlohmann::json envelope;
|
||||||
|
envelope["data"] = data;
|
||||||
|
// The page may still be loading, so wait briefly for the bridge to define __orcaDispatch.
|
||||||
|
WebView::RunScript(m_browser, wxString::Format(
|
||||||
|
"(function dispatch(payload, attempts) {\n"
|
||||||
|
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
|
||||||
|
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
|
||||||
|
"})(%s, 0);",
|
||||||
|
wxString::FromUTF8(envelope.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace))));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::fire_close()
|
||||||
|
{
|
||||||
|
if (m_closing)
|
||||||
|
return;
|
||||||
|
m_closing = true;
|
||||||
|
if (m_on_close) {
|
||||||
|
CloseHandler on_close = std::move(m_on_close);
|
||||||
|
m_on_close = nullptr;
|
||||||
|
on_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::request_close()
|
||||||
|
{
|
||||||
|
if (m_closing)
|
||||||
|
return;
|
||||||
|
fire_close();
|
||||||
|
// A close requested by the page arrives inside the web view's own script-message callback,
|
||||||
|
// which must return before the web view is destroyed.
|
||||||
|
CallAfter([this]() { remove_pane(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::destroy_for_plugin()
|
||||||
|
{
|
||||||
|
m_closing = true;
|
||||||
|
m_on_close = nullptr;
|
||||||
|
remove_pane();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::remove_pane()
|
||||||
|
{
|
||||||
|
if (Plater* plater = wxGetApp().plater())
|
||||||
|
plater->remove_plugin_pane(this);
|
||||||
|
else
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <wx/panel.h>
|
||||||
|
#include <wx/webview.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// Name of a plugin's pane in the Plater's dock manager. It stays the same across sessions, so the
|
||||||
|
// saved window layout can put the pane back, and it never contains a wxAuiManager layout delimiter.
|
||||||
|
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title);
|
||||||
|
|
||||||
|
// The pane part saved for `pane_name` in a wxAuiManager layout string, in the form
|
||||||
|
// wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane.
|
||||||
|
std::string plugin_pane_layout_entry(const std::string& layout, const std::string& pane_name);
|
||||||
|
|
||||||
|
// Plugin-supplied HTML hosted in a pane of the Plater's dock manager, bridged to the plugin through
|
||||||
|
// the same window.orca API as PluginWebDialog. Python-agnostic for the same reason: it can be
|
||||||
|
// destroyed on the main thread without the GIL, so its hooks must not capture pybind11 objects.
|
||||||
|
class PluginDockPanel : public wxPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
||||||
|
using CloseHandler = std::function<void()>;
|
||||||
|
|
||||||
|
// on_close fires once, on a user or page initiated close. on_destroyed runs from the destructor
|
||||||
|
// on every path and must touch host-side state only.
|
||||||
|
PluginDockPanel(wxWindow* parent,
|
||||||
|
const std::string& html,
|
||||||
|
MessageHandler on_message,
|
||||||
|
CloseHandler on_close,
|
||||||
|
CloseHandler on_destroyed);
|
||||||
|
~PluginDockPanel() override;
|
||||||
|
|
||||||
|
// Main thread only.
|
||||||
|
void push_message(const nlohmann::json& data);
|
||||||
|
// Fires on_close, then removes the pane.
|
||||||
|
void request_close();
|
||||||
|
// Removes the pane without firing on_close, for plugin unload.
|
||||||
|
void destroy_for_plugin();
|
||||||
|
// Fires on_close at most once. The Plater calls it when the pane's own close button is used.
|
||||||
|
void fire_close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void on_bootstrap_event(wxWebViewEvent& event);
|
||||||
|
void on_script_message(wxWebViewEvent& event);
|
||||||
|
void on_webview_recreated(wxCommandEvent& event);
|
||||||
|
void remove_pane();
|
||||||
|
|
||||||
|
wxWebView* m_browser{nullptr};
|
||||||
|
std::string m_html;
|
||||||
|
bool m_content_loaded{false};
|
||||||
|
bool m_closing{false};
|
||||||
|
MessageHandler m_on_message;
|
||||||
|
CloseHandler m_on_close;
|
||||||
|
CloseHandler m_on_destroyed;
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -57,6 +57,10 @@ wxString web_base_url()
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
const char* PluginWebDialog::bridge_user_script() { return ORCA_BRIDGE_JS; }
|
||||||
|
|
||||||
|
wxString PluginWebDialog::content_base_url() { return web_base_url(); }
|
||||||
|
|
||||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const std::string& html,
|
const std::string& html,
|
||||||
@@ -75,7 +79,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
|||||||
{
|
{
|
||||||
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
// is swapped in via SetPage once the bootstrap finishes loading.
|
||||||
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
|
create_webview(BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
||||||
|
|
||||||
// Paint the window/webview in the themed background so there is no white
|
// Paint the window/webview in the themed background so there is no white
|
||||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ public:
|
|||||||
static void request_close(PluginWebDialog* dialog);
|
static void request_close(PluginWebDialog* dialog);
|
||||||
static void destroy_for_plugin(PluginWebDialog* dialog);
|
static void destroy_for_plugin(PluginWebDialog* dialog);
|
||||||
|
|
||||||
|
// Shared with PluginDockPanel: the bundled blank page a plugin web view loads before the plugin
|
||||||
|
// HTML is swapped in, the window.orca bridge, and the base URL the plugin HTML is loaded against.
|
||||||
|
static constexpr const char* BOOTSTRAP_PAGE = "web/dialog/PluginWebDialog/blank.html";
|
||||||
|
static const char* bridge_user_script();
|
||||||
|
static wxString content_base_url();
|
||||||
|
|
||||||
// Push a payload to the page; delivered to handlers registered via
|
// Push a payload to the page; delivered to handlers registered via
|
||||||
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
|
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
|
||||||
void push_message(const nlohmann::json& data);
|
void push_message(const nlohmann::json& data);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ if(document.documentElement)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::string WebViewHostDialog::theme_apply_script() { return host_theme_apply_js(); }
|
||||||
|
|
||||||
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
|
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
|
||||||
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
|
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
|
||||||
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
|
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ public:
|
|||||||
// Shared by modeless Pages tabs and PluginWebDialog.
|
// Shared by modeless Pages tabs and PluginWebDialog.
|
||||||
static std::string theme_user_script();
|
static std::string theme_user_script();
|
||||||
static std::string plugin_defaults_user_script();
|
static std::string plugin_defaults_user_script();
|
||||||
|
// Re-themes an already-loaded page in place, for web views hosted outside a dialog.
|
||||||
|
static std::string theme_apply_script();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxWebView* browser() const { return m_browser; }
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <slic3r/GUI/GUI_App.hpp>
|
#include <slic3r/GUI/GUI_App.hpp>
|
||||||
#include <slic3r/GUI/MainFrame.hpp>
|
#include <slic3r/GUI/MainFrame.hpp>
|
||||||
#include <slic3r/GUI/MsgDialog.hpp>
|
#include <slic3r/GUI/MsgDialog.hpp>
|
||||||
|
#include <slic3r/GUI/Plater.hpp>
|
||||||
|
#include <slic3r/GUI/PluginDockPanel.hpp>
|
||||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||||
#include <slic3r/GUI/NotificationManager.hpp>
|
#include <slic3r/GUI/NotificationManager.hpp>
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
#include <wx/defs.h>
|
#include <wx/defs.h>
|
||||||
#include <wx/window.h>
|
#include <wx/window.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <future>
|
#include <future>
|
||||||
@@ -109,6 +112,25 @@ GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The plugin's on_close: fired only on a user/JS-initiated close (not forced teardown), while the
|
||||||
|
// window is alive. Empty if the plugin passed None.
|
||||||
|
std::function<void()> make_close_adapter(const CallablePtr& holder)
|
||||||
|
{
|
||||||
|
if (!holder)
|
||||||
|
return nullptr;
|
||||||
|
return [holder]() {
|
||||||
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
holder->fn();
|
||||||
|
} catch (py::error_already_set& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
||||||
// owning plugin so all of a plugin's UI can be torn down on unload.
|
// owning plugin so all of a plugin's UI can be torn down on unload.
|
||||||
@@ -350,22 +372,7 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
if (!UiRegistry::instance().is_open(new_id))
|
if (!UiRegistry::instance().is_open(new_id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
|
GUI::PluginWebDialog::CloseHandler on_close = make_close_adapter(close_holder);
|
||||||
// teardown), while the dialog is alive. Empty if the plugin passed None.
|
|
||||||
GUI::PluginWebDialog::CloseHandler on_close;
|
|
||||||
if (close_holder) {
|
|
||||||
on_close = [close_holder]() {
|
|
||||||
PythonGILState gil;
|
|
||||||
if (!gil)
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
close_holder->fn();
|
|
||||||
} catch (py::error_already_set& e) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
||||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
|
|
||||||
@@ -387,14 +394,69 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
return py::cast(UiWindowHandle{new_id});
|
return py::cast(UiWindowHandle{new_id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// orca.host.ui.create_dock_panel + UiDockPanel handle
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
constexpr const char* DOCK_POSITIONS[] = {"left", "right", "bottom", "float"};
|
||||||
|
|
||||||
|
struct UiDockPanelHandle
|
||||||
|
{
|
||||||
|
int id{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
py::object ui_create_dock_panel(const std::string& html, const std::string& title, const std::string& dock, int width,
|
||||||
|
int height, py::object on_message, py::object on_close)
|
||||||
|
{
|
||||||
|
if (std::find(std::begin(DOCK_POSITIONS), std::end(DOCK_POSITIONS), dock) == std::end(DOCK_POSITIONS))
|
||||||
|
throw std::invalid_argument("orca.host.ui.create_dock_panel dock must be \"left\", \"right\", \"bottom\" or \"float\"");
|
||||||
|
|
||||||
|
auto msg_adapter = make_message_adapter(std::move(on_message));
|
||||||
|
CallablePtr close_holder = make_holder(std::move(on_close));
|
||||||
|
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||||
|
const int w = width > 0 ? width : 320;
|
||||||
|
const int h = height > 0 ? height : 480;
|
||||||
|
|
||||||
|
if (wxTheApp == nullptr)
|
||||||
|
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||||
|
|
||||||
|
// Deferred and pre-bound for the same reasons as create_window().
|
||||||
|
const int new_id = UiRegistry::instance().reserve_id();
|
||||||
|
UiRegistry::instance().bind(new_id, nullptr, plugin_key);
|
||||||
|
|
||||||
|
GUI::wxGetApp().CallAfter([new_id, plugin_key, html, title, dock, w, h,
|
||||||
|
msg_adapter = std::move(msg_adapter),
|
||||||
|
close_holder = std::move(close_holder)]() mutable {
|
||||||
|
if (!UiRegistry::instance().is_open(new_id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||||
|
if (plater == nullptr || GUI::wxGetApp().is_closing()) {
|
||||||
|
UiRegistry::instance().remove(new_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
|
auto* panel = new GUI::PluginDockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
||||||
|
std::move(on_destroyed));
|
||||||
|
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
||||||
|
plater->add_plugin_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
||||||
|
wxSize(w, h), [panel]() { panel->fire_close(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
return py::cast(UiDockPanelHandle{new_id});
|
||||||
|
}
|
||||||
|
|
||||||
void handle_post(int id, py::object data)
|
void handle_post(int id, py::object data)
|
||||||
{
|
{
|
||||||
if (wxTheApp == nullptr)
|
if (wxTheApp == nullptr)
|
||||||
return;
|
return;
|
||||||
json j = py_to_json(data); // GIL held (binding body)
|
json j = py_to_json(data); // GIL held (binding body)
|
||||||
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
||||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||||
GUI::PluginWebDialog::post_message(d, j);
|
if (auto* panel = dynamic_cast<GUI::PluginDockPanel*>(window))
|
||||||
|
panel->push_message(j);
|
||||||
|
else
|
||||||
|
GUI::PluginWebDialog::post_message(dynamic_cast<GUI::PluginWebDialog*>(window), j);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +465,23 @@ void handle_close(int id)
|
|||||||
if (wxTheApp == nullptr)
|
if (wxTheApp == nullptr)
|
||||||
return;
|
return;
|
||||||
GUI::wxGetApp().CallAfter([id]() {
|
GUI::wxGetApp().CallAfter([id]() {
|
||||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||||
GUI::PluginWebDialog::request_close(d);
|
if (auto* panel = dynamic_cast<GUI::PluginDockPanel*>(window))
|
||||||
|
panel->request_close();
|
||||||
|
else
|
||||||
|
GUI::PluginWebDialog::request_close(dynamic_cast<GUI::PluginWebDialog*>(window));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_show(int id, bool show)
|
||||||
|
{
|
||||||
|
if (wxTheApp == nullptr)
|
||||||
|
return;
|
||||||
|
GUI::wxGetApp().CallAfter([id, show]() {
|
||||||
|
auto* panel = UiRegistry::instance().get_as<GUI::PluginDockPanel>(id);
|
||||||
|
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||||
|
if (panel != nullptr && plater != nullptr)
|
||||||
|
plater->show_plugin_pane(panel, show);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,6 +640,31 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
|||||||
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
|
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
|
||||||
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
|
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
|
||||||
|
|
||||||
|
py::class_<UiDockPanelHandle>(ui, "UiDockPanel", "Handle to a dockable plugin HTML panel created by create_dock_panel().")
|
||||||
|
.def_property_readonly("id", [](const UiDockPanelHandle& h) { return h.id; })
|
||||||
|
.def(
|
||||||
|
"post", [](const UiDockPanelHandle& h, py::object data) { handle_post(h.id, std::move(data)); },
|
||||||
|
py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).")
|
||||||
|
.def(
|
||||||
|
"show", [](const UiDockPanelHandle& h) { handle_show(h.id, true); }, "Show the panel again after hide().")
|
||||||
|
.def(
|
||||||
|
"hide", [](const UiDockPanelHandle& h) { handle_show(h.id, false); }, "Hide the panel without closing it.")
|
||||||
|
.def(
|
||||||
|
"close", [](const UiDockPanelHandle& h) { handle_close(h.id); }, "Close the panel (fires on_close).")
|
||||||
|
.def(
|
||||||
|
"is_open", [](const UiDockPanelHandle& h) { return UiRegistry::instance().is_open(h.id); },
|
||||||
|
"Return True until the panel is closed; a hidden panel is still open.");
|
||||||
|
|
||||||
|
ui.def("create_dock_panel", &ui_create_dock_panel, py::arg("html"), py::arg("title") = "OrcaSlicer",
|
||||||
|
py::arg("dock") = "right", py::arg("width") = 320, py::arg("height") = 480,
|
||||||
|
py::arg("on_message") = py::none(), py::arg("on_close") = py::none(),
|
||||||
|
"Open an HTML panel docked beside the 3D view and return a UiDockPanel. dock is \"left\", \"right\", "
|
||||||
|
"\"bottom\" or \"float\", and width/height are in DIPs; the user can move and resize the panel, and the "
|
||||||
|
"saved window layout remembers both. The panel belongs to the Prepare and Preview tabs. on_message(data) "
|
||||||
|
"is called on the UI thread when the page posts; window.orca.close() or the panel's close button closes "
|
||||||
|
"it and calls on_close(). A post() made before the page has loaded can be dropped, so have the page "
|
||||||
|
"request its first data.");
|
||||||
|
|
||||||
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
|
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
|
||||||
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
|
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
|
||||||
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
||||||
@@ -632,6 +734,8 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
|||||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||||
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
|
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
|
||||||
GUI::PluginWebDialog::destroy_for_plugin(dialog);
|
GUI::PluginWebDialog::destroy_for_plugin(dialog);
|
||||||
|
else if (auto* panel = dynamic_cast<GUI::PluginDockPanel*>(window))
|
||||||
|
panel->destroy_for_plugin();
|
||||||
else if (window != nullptr)
|
else if (window != nullptr)
|
||||||
window->Destroy();
|
window->Destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <libslic3r/Model.hpp>
|
#include <libslic3r/Model.hpp>
|
||||||
#include <libslic3r/PresetBundle.hpp>
|
#include <libslic3r/PresetBundle.hpp>
|
||||||
#include <libslic3r/TriangleMesh.hpp>
|
#include <libslic3r/TriangleMesh.hpp>
|
||||||
|
#include <slic3r/GUI/PluginDockPanel.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||||
|
|
||||||
#include "python_test_support.hpp"
|
#include "python_test_support.hpp"
|
||||||
@@ -141,6 +142,8 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
|||||||
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
||||||
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
||||||
CHECK(has_attr(ui, "UiWindow"));
|
CHECK(has_attr(ui, "UiWindow"));
|
||||||
|
CHECK(has_attr(ui, "create_dock_panel"));
|
||||||
|
CHECK(has_attr(ui, "UiDockPanel"));
|
||||||
|
|
||||||
// With no wx application the UI calls marshal to a main thread that does not
|
// With no wx application the UI calls marshal to a main thread that does not
|
||||||
// exist here; they must fail cleanly with a clear error, not crash.
|
// exist here; they must fail cleanly with a clear error, not crash.
|
||||||
@@ -151,6 +154,45 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
|||||||
CHECK(error.matches(PyExc_RuntimeError));
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>");
|
||||||
|
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unknown dock position is rejected before the application is needed.
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>", py::arg("dock") = "top");
|
||||||
|
FAIL("orca.host.ui.create_dock_panel accepted an unknown dock position");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_ValueError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Plugin pane names identify the plugin and title without layout delimiters", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using Slic3r::GUI::plugin_pane_name;
|
||||||
|
|
||||||
|
CHECK(plugin_pane_name("dock_demo", "Scene") == "plugin:dock_demo:Scene");
|
||||||
|
CHECK(plugin_pane_name("key", "a|b;c=d\\e").find_first_of("|;=\\") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A plugin pane's saved layout entry is found by pane name", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using Slic3r::GUI::plugin_pane_layout_entry;
|
||||||
|
|
||||||
|
const std::string sidebar = "name=sidebar;caption=;state=2099196;dir=4;layer=0;row=0;pos=0;bestw=390;besth=900";
|
||||||
|
// The caption holds an escaped '|', which must not end the entry.
|
||||||
|
const std::string plugin = "name=plugin:demo:Scene;caption=Scene \\| stats;state=2099198;dir=2;layer=0;row=1;pos=0;bestw=320;besth=480";
|
||||||
|
const std::string layout = "layout3|" + sidebar + "|" + plugin + "|dock_size(4,0,0)=392|";
|
||||||
|
|
||||||
|
CHECK(plugin_pane_layout_entry(layout, "plugin:demo:Scene") == plugin);
|
||||||
|
CHECK(plugin_pane_layout_entry(layout, "sidebar") == sidebar);
|
||||||
|
CHECK(plugin_pane_layout_entry(layout, "plugin:demo").empty());
|
||||||
|
CHECK(plugin_pane_layout_entry("", "plugin:demo:Scene").empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
||||||
|
|||||||
@@ -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