mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
Merge upstream into the parallel printing work
Brings in nine commits, including stricter slice validation of custom G-code and filename formats across system profiles, OBJ and DRC import hardening, and the Ender-3 V3 SE extrusion mode fix. None of them touch the files this branch changes.
This commit is contained in:
@@ -183,9 +183,11 @@ jobs:
|
||||
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
|
||||
artifact: ${{ github.sha }}-tests-macos-arm64
|
||||
test-dir: build/arm64/tests
|
||||
# Slice a two-colour cube through every shipped printer so all custom g-code
|
||||
# (change_filament_gcode, machine start/end, etc.) is expanded - catches
|
||||
# slicing regressions the static profile checks and unit tests can't see.
|
||||
# Slice a two-colour cube through every shipped printer, and through every
|
||||
# system process/filament whose templates no printer's own slice reaches, so
|
||||
# every custom g-code and filename_format shipped is expanded (names in {if}
|
||||
# branches not taken included) - catches slicing regressions the static
|
||||
# profile checks and unit tests can't see.
|
||||
# Profile-only PRs are covered by check_profiles.yml's nightly binary; this
|
||||
# covers src/engine PRs with the PR-built binary.
|
||||
slice_check_linux:
|
||||
|
||||
@@ -74,8 +74,10 @@ jobs:
|
||||
set +e
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
# Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
|
||||
# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see.
|
||||
# Slice a two-colour cube through every printer, and through every system process/filament whose
|
||||
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
|
||||
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
|
||||
# invalid-flow bugs the static checks above cannot see.
|
||||
- name: validate slice (expand custom g-code)
|
||||
id: validate_slice
|
||||
continue-on-error: true
|
||||
|
||||
@@ -44,6 +44,14 @@ jobs:
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
# run_unit_tests.sh installs the plugin tests' numpy with the uv the build stages
|
||||
# beside them; the Windows arm64 build bundles none, so put one on PATH there.
|
||||
- name: Install uv
|
||||
if: runner.os == 'Windows' && runner.arch == 'ARM64'
|
||||
uses: astral-sh/setup-uv@v10.2.0
|
||||
with:
|
||||
version: "0.11.21" # ORCA_UV_VERSION in CMakeLists.txt
|
||||
enable-cache: false
|
||||
- uses: lukka/get-cmake@latest
|
||||
with:
|
||||
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
|
||||
|
||||
+3
-1
@@ -52,4 +52,6 @@ internal_docs/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.opc
|
||||
docs/superpowers/
|
||||
/.test/
|
||||
docs/superpowers/
|
||||
ctest_results.xml
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -121,7 +121,7 @@
|
||||
"Generic PLA @Creality Ender-3V3-all"
|
||||
],
|
||||
"file_start_gcode": ";FLAVOR:Marlin\\n;TIME:{print_time_total_sec}\\n;Filament used:{used_filament_length}m\\n;Layer height:{layer_height}",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nG90 ;Absolute positioning \nM82 ;Absolute extrusion mode \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_end_gcode": "G91 ;Relative positionning \nG1 E-2 F2700 ;Retract a bit \nG1 E-2 Z0.2 F2400 ;Retract and raise Z \nG1 X5 Y5 F3000 ;Wipe out \nG1 Z10 ;Raise Z more \nG90 ;Absolute positionning \n \nG1 X0 Y220 ;Present print \nM106 S0 ;Turn-off fan \nM104 S0 ;Turn-off hotend \nM140 S0 ;Turn-off bed \n \nM84 X Y E ;Disable all steppers but Z",
|
||||
"scan_first_layer": "0",
|
||||
"disable_m73": "1",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"Generic PLA @Creality Ender-3V3-all"
|
||||
],
|
||||
"file_start_gcode": ";FLAVOR:Marlin\\n;TIME:{print_time_total_sec}\\n;Filament used:{used_filament_length}m\\n;Layer height:{layer_height}",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nG90 ;Absolute positioning \nM82 ;Absolute extrusion mode \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_end_gcode": "G91 ;Relative positionning \nG1 E-2 F2700 ;Retract a bit \nG1 E-2 Z0.2 F2400 ;Retract and raise Z \nG1 X5 Y5 F3000 ;Wipe out \nG1 Z10 ;Raise Z more \nG90 ;Absolute positionning \n \nG1 X0 Y220 ;Present print \nM106 S0 ;Turn-off fan \nM104 S0 ;Turn-off hotend \nM140 S0 ;Turn-off bed \n \nM84 X Y E ;Disable all steppers but Z",
|
||||
"scan_first_layer": "0",
|
||||
"disable_m73": "1",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"Generic PLA @Creality Ender-3V3-all"
|
||||
],
|
||||
"file_start_gcode": ";FLAVOR:Marlin\\n;TIME:{print_time_total_sec}\\n;Filament used:{used_filament_length}m\\n;Layer height:{layer_height}",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nG90 ;Absolute positioning \nM82 ;Absolute extrusion mode \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_end_gcode": "G91 ;Relative positionning \nG1 E-2 F2700 ;Retract a bit \nG1 E-2 Z0.2 F2400 ;Retract and raise Z \nG1 X5 Y5 F3000 ;Wipe out \nG1 Z10 ;Raise Z more \nG90 ;Absolute positionning \n \nG1 X0 Y220 ;Present print \nM106 S0 ;Turn-off fan \nM104 S0 ;Turn-off hotend \nM140 S0 ;Turn-off bed \n \nM84 X Y E ;Disable all steppers but Z",
|
||||
"scan_first_layer": "0",
|
||||
"disable_m73": "1",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"Generic PLA @Creality Ender-3V3-all"
|
||||
],
|
||||
"file_start_gcode": ";FLAVOR:Marlin\\n;TIME:{print_time_total_sec}\\n;Filament used:{used_filament_length}m\\n;Layer height:{layer_height}",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_start_gcode": "M220 S100 ;Reset Feedrate \nM221 S100 ;Reset Flowrate \n \nG90 ;Absolute positioning \nM82 ;Absolute extrusion mode \n \nM104 S[nozzle_temperature_initial_layer] ;Set final nozzle temp \nM190 S[bed_temperature_initial_layer_single] ;Set and wait for bed temp to stabilize \nG28 ;Home \nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position \nM109 S[nozzle_temperature_initial_layer] ;Wait for nozzle temp to stabilize \nG1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line \nG1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little \nG1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line \nG92 E0 ;Reset Extruder \nG1 E-1.0000 F1800 ;Retract a bit \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 E0.0000 F1800",
|
||||
"machine_end_gcode": "G91 ;Relative positionning \nG1 E-2 F2700 ;Retract a bit \nG1 E-2 Z0.2 F2400 ;Retract and raise Z \nG1 X5 Y5 F3000 ;Wipe out \nG1 Z10 ;Raise Z more \nG90 ;Absolute positionning \n \nG1 X0 Y220 ;Present print \nM106 S0 ;Turn-off fan \nM104 S0 ;Turn-off hotend \nM140 S0 ;Turn-off bed \n \nM84 X Y E ;Disable all steppers but Z",
|
||||
"scan_first_layer": "0",
|
||||
"disable_m73": "1",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"1"
|
||||
],
|
||||
"support_multi_bed_types": "1",
|
||||
"template_custom_gcode": ";;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n{if curr_bed_type==\"Textured PEI Plate\"}\n SET_GCODE_OFFSET Z=-0.00\n{else}\nSET_GCODE_OFFSET Z=0.0\n{endif}\n{if curr_bed_type==\"Cool Plate\"}\n\n;available bed types are:\n;\"Cool Plate\"\n;\"Engineering Plate\"\n;\"High Temp Plate\"\n;\"Textured PEI Plate\"\n;;;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n",
|
||||
"template_custom_gcode": ";;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n{if curr_bed_type==\"Textured PEI Plate\"}\n SET_GCODE_OFFSET Z=-0.00\n{else}\nSET_GCODE_OFFSET Z=0.0\n{endif}\n\n;available bed types are:\n;\"Cool Plate\"\n;\"Engineering Plate\"\n;\"High Temp Plate\"\n;\"Textured PEI Plate\"\n;;;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n",
|
||||
"z_hop_types": "Normal Lift",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
|
||||
"default_print_profile": "0.16mm Optimal @Bambu Lab X1 Carbon 0.4 nozzle",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
# This file is made to support the unit tests workflow.
|
||||
# It should only require the directories build/tests, scripts/, and tests/ to function,
|
||||
# and cmake (with ctest) installed.
|
||||
# and cmake (with ctest) installed -- plus network access to PyPI whenever numpy has to
|
||||
# be installed into a freshly built test tree (see below).
|
||||
# (otherwise, update the workflow too, but try to avoid to keep things self-contained)
|
||||
#
|
||||
# Usage: run_unit_tests.sh [TEST_DIR] [BUILD_CONFIG]
|
||||
@@ -18,6 +19,52 @@ cd "${ROOT_DIR}" || exit 1
|
||||
TEST_DIR="${1:-build/tests}"
|
||||
BUILD_CONFIG="${2:-}"
|
||||
|
||||
# The slic3rutils plugin-host tests build numpy arrays through the CPython copied next
|
||||
# to the test binary (see tests/slic3rutils/CMakeLists.txt), which ships no numpy.
|
||||
# Install it with the uv staged beside that runtime -- the tool the app installs plugin
|
||||
# dependencies with -- straight into the interpreter's own site-packages: no pip needed
|
||||
# in the runtime, no PYTHONPATH. Re-checked every run because a rebuild of the test
|
||||
# target re-copies the runtime; needs network whenever it installs. Pinned so a numpy
|
||||
# release cannot change results on its own.
|
||||
NUMPY_VERSION="2.5.3"
|
||||
|
||||
# Without numpy those tests assert the numpy-absent error path instead, so a local run
|
||||
# only warns. Under CI it fails the run, which would otherwise stay green while silently
|
||||
# dropping the array coverage. (The Flatpak leg runs this inside `flatpak build`, whose
|
||||
# minimal environment has no CI, and its offline build stages no uv, so numpy stays
|
||||
# best-effort there.)
|
||||
numpy_unavailable() {
|
||||
if [ -n "${CI:-}" ]; then
|
||||
echo "error: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "warning: $1; the numpy-backed binding tests will cover only the numpy-absent path."
|
||||
}
|
||||
|
||||
has_pinned_numpy() {
|
||||
"${python_exe}" -c "import sys, numpy; sys.exit(numpy.__version__ != '${NUMPY_VERSION}')" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
find_args=("${TEST_DIR}" \( -path '*/python/bin/python3' -o -path '*/python/python.exe' \))
|
||||
# Multi-config trees hold one copy per configuration; only bootstrap the one being run.
|
||||
[ -n "${BUILD_CONFIG}" ] && find_args+=(-path "*/${BUILD_CONFIG}/*")
|
||||
python_exe="$(find "${find_args[@]}" -print -quit 2>/dev/null)"
|
||||
|
||||
if [ -z "${python_exe}" ]; then
|
||||
numpy_unavailable "no bundled Python under ${TEST_DIR}"
|
||||
elif ! has_pinned_numpy; then
|
||||
uv_exe="${python_exe%/python/*}/tools/uv/uv"
|
||||
# Builds that bundle no uv (Windows arm64) fall back to one on PATH.
|
||||
[ -x "${uv_exe}" ] || uv_exe="$(command -v uv)"
|
||||
echo "Installing numpy ${NUMPY_VERSION} into the embedded test interpreter (${python_exe})..."
|
||||
if [ -z "${uv_exe}" ]; then
|
||||
numpy_unavailable "no uv staged beside the tests or on PATH"
|
||||
elif ! "${uv_exe}" pip install --python "${python_exe}" --only-binary :all: "numpy==${NUMPY_VERSION}" \
|
||||
|| ! has_pinned_numpy; then
|
||||
numpy_unavailable "could not install numpy ${NUMPY_VERSION} into ${python_exe}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run the whole suite, excluding tests tagged [NotWorking] and tests labelled RequiresApp,
|
||||
# which run the built orca-slicer binary that this directory does not contain.
|
||||
# --no-tests=error fails the job if the filter matches nothing (instead of passing green).
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/log/core.hpp>
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -160,37 +162,62 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
|
||||
return rigid ? move : Vec2d::Zero();
|
||||
}
|
||||
|
||||
// Slice one cube that switches from filament 1 to filament 2 partway up, so exactly one
|
||||
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
|
||||
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
|
||||
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
|
||||
// topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws
|
||||
// Slic3r::PlaceholderParserError from export.
|
||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
|
||||
// Slice cubes that switch from filament 1 to filament 2 partway up, so a filament change fires, then
|
||||
// export. The change drives the printer's own change_filament_gcode: on a single-nozzle machine it rides
|
||||
// the AMS prime tower (append_tcr), on a multi-nozzle machine it routes through the nozzle swap
|
||||
// (set_extruder / append_tcr2) - the engine picks the path from the printer's topology, so one model
|
||||
// covers both. The layer-by-layer slice also pauses at z 2 and runs the template custom g-code at z 7, so
|
||||
// machine_pause_gcode and template_custom_gcode expand, and turns clumping detection on for a printer that
|
||||
// ships wrapping_detection_gcode. The by-object slice prints two cubes in turn without a prime tower:
|
||||
// printing_by_object_gcode fires before the second one, and the filament change goes through set_extruder.
|
||||
// Either way the output file name is built after export, which expands filename_format with the final
|
||||
// print statistics. An undefined placeholder in any shipped custom g-code throws
|
||||
// Slic3r::PlaceholderParserError.
|
||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
|
||||
{
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
const Vec2d cube_min = center - Vec2d(5., 5.) + place_wipe_tower(cfg, center);
|
||||
TriangleMesh m = make_cube(10, 10, 10);
|
||||
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
std::vector<Vec2d> cube_mins;
|
||||
if (by_object) {
|
||||
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
|
||||
// smooth timelapse. No skirt, so the two cubes' own skirts cannot overlap.
|
||||
cfg.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
|
||||
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(false));
|
||||
cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
|
||||
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
|
||||
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
|
||||
} else {
|
||||
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
|
||||
if (!cfg.opt_string("wrapping_detection_gcode").empty())
|
||||
cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true));
|
||||
cube_mins = {center - Vec2d(5., 5.) + place_wipe_tower(cfg, center)};
|
||||
}
|
||||
|
||||
Model model;
|
||||
Print print;
|
||||
ModelObject *obj = model.add_object();
|
||||
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
|
||||
obj->add_volume(m);
|
||||
obj->add_instance();
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
|
||||
print.is_BBL_printer() = is_bbl;
|
||||
obj->ensure_on_bed();
|
||||
print.auto_assign_extruders(obj);
|
||||
for (const Vec2d &cube_min : cube_mins) {
|
||||
TriangleMesh m = make_cube(10, 10, 10);
|
||||
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
|
||||
ModelObject *obj = model.add_object();
|
||||
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
|
||||
obj->add_volume(m);
|
||||
obj->add_instance();
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
obj->ensure_on_bed();
|
||||
print.auto_assign_extruders(obj);
|
||||
}
|
||||
// Custom g-codes per print_z apply to layer-by-layer printing only (ToolOrdering::assign_custom_gcodes).
|
||||
if (!by_object)
|
||||
model.plates_custom_gcodes[model.curr_plate_index].gcodes = {{2., CustomGCode::PausePrint, 1, "", ""},
|
||||
{7., CustomGCode::Template, 1, "", ""}};
|
||||
print.apply(model, cfg);
|
||||
print.validate();
|
||||
|
||||
@@ -205,6 +232,7 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
|
||||
in.close();
|
||||
boost::system::error_code ec;
|
||||
fs::remove(tmp, ec);
|
||||
print.output_filename(); // names the export as the app does, expanding filename_format
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -264,18 +292,119 @@ void install_slice_context_log_sink()
|
||||
logging::core::get()->add_sink(sink);
|
||||
}
|
||||
|
||||
// Size the filament slots for the current selection and build the config every slice uses.
|
||||
DynamicPrintConfig slice_config(PresetBundle &bundle)
|
||||
{
|
||||
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
|
||||
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
|
||||
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
|
||||
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
|
||||
|
||||
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
|
||||
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
|
||||
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
|
||||
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
|
||||
// while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in
|
||||
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
|
||||
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
|
||||
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
|
||||
if (pin_filament_map) {
|
||||
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
|
||||
for (size_t i = 0; i < fmap.size(); ++i)
|
||||
fmap[i] = int(i % nozzles) + 1;
|
||||
}
|
||||
|
||||
DynamicPrintConfig cfg = bundle.full_config();
|
||||
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
|
||||
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
|
||||
// stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce
|
||||
// the variant mismatch this pinning avoids).
|
||||
if (pin_filament_map)
|
||||
cfg.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
|
||||
|
||||
// full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic
|
||||
// 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves
|
||||
// filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments
|
||||
// fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real
|
||||
// 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The
|
||||
// slice output is unaffected: the duplicated filament's per-variant values are identical to the first.
|
||||
if (auto *variants = cfg.option<ConfigOptionStrings>("filament_extruder_variant")) {
|
||||
auto &self_index = cfg.option<ConfigOptionInts>("filament_self_index", true)->values;
|
||||
if (self_index.size() != variants->size()) {
|
||||
self_index.resize(variants->size());
|
||||
for (size_t i = 0; i < self_index.size(); ++i)
|
||||
self_index[i] = int(i) + 1;
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Slice the current selection and log any failure against `what` (the printer, and the extra preset if
|
||||
// any). With an outdir, the g-code is also saved there as "<file_base>.gcode". Returns the g-code, or an
|
||||
// empty string on failure.
|
||||
std::string slice_selection(PresetBundle &bundle, const std::string &what, bool by_object, const std::string &outdir, const std::string &file_base)
|
||||
{
|
||||
try {
|
||||
const std::string out = slice_two_color_cube_and_export(slice_config(bundle), bundle.is_bbl_vendor(), by_object);
|
||||
if (!outdir.empty() && !out.empty())
|
||||
save_string_file(fs::path(outdir) / (file_base + ".gcode"), out);
|
||||
if (out.empty() || out.find("G1") == std::string::npos) {
|
||||
BOOST_LOG_TRIVIAL(error) << what << " produced no g-code";
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << what << " failed to slice: " << ex.what();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// The templates a preset carries, as (key, text): every non-empty *_gcode text, and filename_format. Each
|
||||
// value of a per-variant key counts, though a slice expands only the values of the variants its printer uses.
|
||||
using Templates = std::set<std::pair<std::string, std::string>>;
|
||||
Templates preset_templates(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
Templates out;
|
||||
for (const std::string &key : cfg.keys()) {
|
||||
if (key != "filename_format" && !boost::algorithm::ends_with(key, "_gcode"))
|
||||
continue;
|
||||
const ConfigOption *opt = cfg.option(key);
|
||||
if (opt->type() == coString) {
|
||||
if (const std::string &text = static_cast<const ConfigOptionString *>(opt)->value; !text.empty())
|
||||
out.emplace(key, text);
|
||||
} else if (opt->type() == coStrings) {
|
||||
for (const std::string &text : static_cast<const ConfigOptionStrings *>(opt)->values)
|
||||
if (!text.empty())
|
||||
out.emplace(key, text);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Slice-and-export a two-colour cube through every shipped printer (optionally scoped to one vendor via
|
||||
// -v). Unlike the static reference/placeholder checks, this expands every custom *_gcode - including
|
||||
// change_filament_gcode at the one filament change - against the printer's fully-resolved config, so
|
||||
// undefined-placeholder / invalid-flow bugs surface here. Reports every offending printer and returns 1
|
||||
// if any failed, 0 otherwise. When outdir is non-empty, each printer's g-code is also written there as
|
||||
// "<vendor>__<printer>.gcode" for manual inspection. The sweep is SEQUENTIAL by necessity:
|
||||
// Print::process() keeps process-global state, so slicing printers concurrently in one process races
|
||||
// even with per-slice Model+Print. Load in validation mode so the vendors are read straight from the -p
|
||||
// profiles dir (no data_dir/system tree) and -v scoping is honoured for free.
|
||||
// undefined-placeholder / invalid-flow bugs surface here. PlaceholderParser::check_inactive_branches is
|
||||
// set for the sweep, so the names in {if} branches a slice does not take must resolve too.
|
||||
//
|
||||
// Each printer is sliced with its own default process and filament. Every other compatible system
|
||||
// process and filament whose templates (filename_format, filament_start_gcode, ...) no slice has expanded
|
||||
// yet is then sliced once on that printer - a filament that compatible_prints limits to another process
|
||||
// with that process - so each template text shipped in any system preset is expanded once. That check runs on the first compatible printer the sweep reaches only, in that printer's hook
|
||||
// contexts: a value-dependent error on another printer, or a hook call site there that sets fewer
|
||||
// variables (a multi-nozzle tool change, say), is not covered. The first printer shipping each
|
||||
// printing_by_object_gcode also slices two cubes by object, the only way that hook fires.
|
||||
//
|
||||
// Reports every failure and returns 1 if any slice failed, 0 otherwise. When outdir is non-empty, each
|
||||
// g-code is also written there as "<vendor>__<printer>[__<preset>].gcode" for manual inspection. The
|
||||
// sweep is SEQUENTIAL by necessity: Print::process() keeps process-global state, so slicing printers
|
||||
// concurrently in one process races even with per-slice Model+Print. Load in validation mode so the
|
||||
// vendors are read straight from the -p profiles dir (no data_dir/system tree) and -v scoping is honoured
|
||||
// for free.
|
||||
int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
{
|
||||
install_slice_context_log_sink();
|
||||
PlaceholderParser::check_inactive_branches = true;
|
||||
|
||||
if (!outdir.empty()) {
|
||||
boost::system::error_code ec;
|
||||
@@ -323,7 +452,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
std::cout << "Slicing " << printers.size() << " printer preset(s)"
|
||||
<< (vendor.empty() ? "" : " for vendor " + vendor) << "..." << std::endl;
|
||||
|
||||
int failures = 0;
|
||||
Templates covered; // the process and filament templates some slice has expanded
|
||||
auto cover = [&covered](const Preset &preset) {
|
||||
const Templates templates = preset_templates(preset.config);
|
||||
covered.insert(templates.begin(), templates.end());
|
||||
};
|
||||
auto is_covered = [&covered](const Preset &preset) {
|
||||
const Templates templates = preset_templates(preset.config);
|
||||
return std::includes(covered.begin(), covered.end(), templates.begin(), templates.end());
|
||||
};
|
||||
int failures = 0, extra_processes = 0, extra_filaments = 0, by_object_slices = 0;
|
||||
for (const auto &[vendor_name, printer] : printers) {
|
||||
g_slice_context = vendor_name + " / " + printer; // tag every engine log line from this slice
|
||||
const bool selected = bundle.printers.select_preset_by_name(printer, /*force=*/true);
|
||||
@@ -347,76 +485,115 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
|
||||
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
|
||||
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
|
||||
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
|
||||
|
||||
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
|
||||
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
|
||||
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
|
||||
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
|
||||
// while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in
|
||||
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
|
||||
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
|
||||
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
|
||||
if (pin_filament_map) {
|
||||
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
|
||||
for (size_t i = 0; i < fmap.size(); ++i)
|
||||
fmap[i] = int(i % nozzles) + 1;
|
||||
}
|
||||
|
||||
DynamicPrintConfig cfg = bundle.full_config();
|
||||
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
|
||||
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
|
||||
// stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce
|
||||
// the variant mismatch this pinning avoids).
|
||||
if (pin_filament_map)
|
||||
cfg.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
|
||||
|
||||
// full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic
|
||||
// 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves
|
||||
// filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments
|
||||
// fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real
|
||||
// 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The
|
||||
// slice output is unaffected: the duplicated filament's per-variant values are identical to the first.
|
||||
if (auto *variants = cfg.option<ConfigOptionStrings>("filament_extruder_variant")) {
|
||||
auto &self_index = cfg.option<ConfigOptionInts>("filament_self_index", true)->values;
|
||||
if (self_index.size() != variants->size()) {
|
||||
self_index.resize(variants->size());
|
||||
for (size_t i = 0; i < self_index.size(); ++i)
|
||||
self_index[i] = int(i) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const std::string out = slice_two_color_cube_and_export(cfg, bundle.is_bbl_vendor());
|
||||
if (!outdir.empty() && !out.empty()) {
|
||||
const fs::path f = fs::path(outdir) / (sanitize_filename(vendor_name) + "__" + sanitize_filename(printer) + ".gcode");
|
||||
save_string_file(f, out);
|
||||
}
|
||||
if (out.empty() || out.find("G1") == std::string::npos) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" produced no g-code";
|
||||
++failures;
|
||||
} else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer
|
||||
<< "\" sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||
++failures;
|
||||
}
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" failed to slice: " << ex.what();
|
||||
const std::string print_name = bundle.prints.get_selected_preset_name();
|
||||
const std::string filament_name = bundle.filaments.get_selected_preset_name();
|
||||
const std::string what = "Printer \"" + printer + "\"";
|
||||
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
|
||||
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
|
||||
++failures;
|
||||
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||
++failures;
|
||||
}
|
||||
cover(bundle.prints.get_selected_preset());
|
||||
cover(bundle.filaments.get_selected_preset());
|
||||
|
||||
const std::string &by_object_gcode = bundle.printers.get_selected_preset().config.opt_string("printing_by_object_gcode");
|
||||
if (!by_object_gcode.empty() && covered.emplace("printing_by_object_gcode", by_object_gcode).second) {
|
||||
++by_object_slices;
|
||||
g_slice_context = vendor_name + " / " + printer + " / by object";
|
||||
if (slice_selection(bundle, what + " printing by object", true, outdir, file_base + "__by_object").empty())
|
||||
++failures;
|
||||
}
|
||||
|
||||
// The first process of this printer that compatible_prints lets an otherwise incompatible filament
|
||||
// use, judged as update_compatible() would with that process selected; empty if there is none.
|
||||
const PresetWithVendorProfile printer_with_vendor = bundle.printers.get_edited_preset_with_vendor_profile();
|
||||
auto limiting_process = [&bundle, &printer_with_vendor](const Preset &filament) -> std::string {
|
||||
const auto *processes = filament.config.option<ConfigOptionStrings>("compatible_prints");
|
||||
if (processes == nullptr || processes->values.empty())
|
||||
return {};
|
||||
const PresetWithVendorProfile filament_with_vendor = bundle.filaments.get_preset_with_vendor_profile(filament);
|
||||
if (!is_compatible_with_printer(filament_with_vendor, printer_with_vendor))
|
||||
return {};
|
||||
for (const std::string &name : processes->values)
|
||||
if (const Preset *process = bundle.prints.find_preset(name);
|
||||
process != nullptr && process->is_visible && process->is_compatible &&
|
||||
is_compatible_with_print(filament_with_vendor, bundle.prints.get_preset_with_vendor_profile(*process), printer_with_vendor))
|
||||
return name;
|
||||
return {};
|
||||
};
|
||||
// The compatibility flags are still this printer's, from update_compatible() above, which judged the
|
||||
// filaments against the printer's own process. A hidden preset cannot be selected, so it is counted,
|
||||
// not sliced.
|
||||
std::vector<std::pair<const Preset *, std::string>> extras; // (extra preset, process to slice it with)
|
||||
size_t hidden = 0;
|
||||
for (const PresetCollection *presets : {&bundle.prints, &bundle.filaments})
|
||||
for (const Preset &preset : presets->get_presets()) {
|
||||
if (!preset.is_system || preset.is_default)
|
||||
continue;
|
||||
std::string process;
|
||||
if (preset.is_compatible)
|
||||
process = preset.type == Preset::TYPE_PRINT ? preset.name : print_name;
|
||||
else if (preset.type == Preset::TYPE_FILAMENT)
|
||||
process = limiting_process(preset);
|
||||
if (process.empty() || is_covered(preset))
|
||||
continue;
|
||||
if (preset.is_visible)
|
||||
extras.emplace_back(&preset, process);
|
||||
else
|
||||
++hidden;
|
||||
}
|
||||
if (hidden > 0)
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer \"" << printer << "\": " << hidden
|
||||
<< " compatible system preset(s) with unexpanded templates are hidden and were not sliced";
|
||||
|
||||
// Each extra slice swaps one preset into the printer's own selection. update_compatible() is not
|
||||
// run again: it could swap filament slots. The filament change is not required to ride the tower
|
||||
// here (a vase-mode process has none): the printer's own slice above already expanded its hooks.
|
||||
for (const auto &[preset, process] : extras) {
|
||||
if (is_covered(*preset))
|
||||
continue; // an earlier extra slice expanded the same texts
|
||||
const bool is_print = preset->type == Preset::TYPE_PRINT;
|
||||
const std::string &filament = is_print ? filament_name : preset->name;
|
||||
std::string extra = std::string(is_print ? "process" : "filament") + " \"" + preset->name + "\"";
|
||||
if (!is_print && process != print_name)
|
||||
extra += " and process \"" + process + "\"";
|
||||
g_slice_context = vendor_name + " / " + printer + " / " + extra;
|
||||
if (is_print)
|
||||
++extra_processes;
|
||||
else
|
||||
++extra_filaments;
|
||||
cover(*preset);
|
||||
bundle.prints.select_preset_by_name(process, /*force=*/true);
|
||||
bundle.filaments.select_preset_by_name(filament, /*force=*/true);
|
||||
bundle.filament_presets.assign(1, bundle.filaments.get_selected_preset_name());
|
||||
bundle.update_multi_material_filament_presets();
|
||||
// select_preset_by_name() falls back to another preset, and still returns true, when it cannot select this one.
|
||||
if (bundle.prints.get_selected_preset_name() != process || bundle.filaments.get_selected_preset_name() != filament) {
|
||||
BOOST_LOG_TRIVIAL(error) << what << " could not select " << extra;
|
||||
++failures;
|
||||
continue;
|
||||
}
|
||||
if (slice_selection(bundle, what + " with " + extra, false, outdir, file_base + "__" + sanitize_filename(preset->name)).empty())
|
||||
++failures;
|
||||
}
|
||||
// Leave the printer's own selection for the next printer to start from, as without the extra slices.
|
||||
bundle.prints.select_preset_by_name(print_name, /*force=*/true);
|
||||
bundle.filaments.select_preset_by_name(filament_name, /*force=*/true);
|
||||
}
|
||||
g_slice_context.clear();
|
||||
|
||||
const int slices = int(printers.size()) + extra_processes + extra_filaments + by_object_slices;
|
||||
std::cout << "Sliced " << printers.size() << " printer preset(s), " << extra_processes << " more process preset(s), "
|
||||
<< extra_filaments << " more filament preset(s) and " << by_object_slices << " printer(s) by object" << std::endl;
|
||||
if (failures > 0) {
|
||||
std::cout << failures << " of " << printers.size() << " printer preset(s) failed to slice" << std::endl;
|
||||
std::cout << failures << " of " << slices << " slice(s) failed" << std::endl;
|
||||
std::cout << "Validation failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << "All " << printers.size() << " printer preset(s) sliced successfully" << std::endl;
|
||||
std::cout << "All " << slices << " slice(s) succeeded" << std::endl;
|
||||
std::cout << "Validation completed successfully" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -435,8 +612,8 @@ int main(int argc, char* argv[])
|
||||
#endif
|
||||
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
|
||||
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
|
||||
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
|
||||
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each printer's g-code to this folder (as <vendor>__<printer>.gcode) for manual inspection. Optional.")
|
||||
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer, and through every other system process and filament preset whose templates no printer's own slice expands, so every custom g-code and filename_format shipped is expanded, names in {if} branches not taken included (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
|
||||
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each slice's g-code to this folder (as <vendor>__<printer>[__<preset>].gcode) for manual inspection. Optional.")
|
||||
("check_filament_subtypes,f", po::bool_switch()->default_value(true), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
|
||||
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
|
||||
// clang-format on
|
||||
|
||||
@@ -48,24 +48,37 @@ bool load_drc(const char *path, TriangleMesh *meshptr)
|
||||
indexed_triangle_set its;
|
||||
|
||||
const PointAttribute *const positions = dracoMesh.GetNamedAttribute(GeometryAttribute::POSITION);
|
||||
if (positions == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_drc: the mesh has no POSITION attribute";
|
||||
return false;
|
||||
}
|
||||
size_t num_vertices = positions->size();
|
||||
its.vertices.reserve(num_vertices);
|
||||
for (AttributeValueIndex i(0); i < num_vertices; ++ i) {
|
||||
float pos[3];
|
||||
positions->ConvertValue<float>(i, 3, pos);
|
||||
if (!positions->ConvertValue<float>(i, 3, pos)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex position";
|
||||
return false;
|
||||
}
|
||||
its.vertices.emplace_back(pos[0], pos[1], pos[2]);
|
||||
}
|
||||
|
||||
// The Draco decoder does not check face indices against the point count.
|
||||
const uint32_t num_points = dracoMesh.num_points();
|
||||
size_t num_faces = dracoMesh.num_faces();
|
||||
its.indices.reserve(num_faces);
|
||||
for (FaceIndex i(0); i < num_faces; ++ i) {
|
||||
Mesh::Face face = dracoMesh.face(i);
|
||||
|
||||
its.indices.emplace_back(
|
||||
positions->mapped_index(face[0]).value(),
|
||||
positions->mapped_index(face[1]).value(),
|
||||
positions->mapped_index(face[2]).value()
|
||||
);
|
||||
const Mesh::Face &face = dracoMesh.face(i);
|
||||
stl_triangle_vertex_indices facet;
|
||||
for (int k = 0; k < 3; ++ k) {
|
||||
const size_t vertex_idx = face[k].value() < num_points ? positions->mapped_index(face[k]).value() : num_vertices;
|
||||
if (vertex_idx >= num_vertices) {
|
||||
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex index";
|
||||
return false;
|
||||
}
|
||||
facet[k] = static_cast<int>(vertex_idx);
|
||||
}
|
||||
its.indices.emplace_back(facet);
|
||||
}
|
||||
|
||||
*meshptr = TriangleMesh(std::move(its));
|
||||
|
||||
@@ -166,10 +166,15 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
obj_info.uv_map_pngs[face_index] = png_name;
|
||||
}
|
||||
if (data.textureCoordinates.size() > 0) {
|
||||
Vec2f uv0(data.textureCoordinates[uvs[0] * 2], data.textureCoordinates[uvs[0] * 2 + 1]);
|
||||
Vec2f uv1(data.textureCoordinates[uvs[1] * 2], data.textureCoordinates[uvs[1] * 2 + 1]);
|
||||
Vec2f uv2(data.textureCoordinates[uvs[2] * 2], data.textureCoordinates[uvs[2] * 2 + 1]);
|
||||
std::array<Vec2f, 3> uv_array{uv0, uv1, uv2};
|
||||
// A face vertex may omit vt or reference a missing one. Fall back to (0, 0) rather than
|
||||
// skipping the face, so obj_info.uvs stays aligned with the face indices.
|
||||
const int uv_count = static_cast<int>(data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH);
|
||||
auto uv_at = [&data, uv_count](int idx) -> Vec2f {
|
||||
if (idx < 0 || idx >= uv_count)
|
||||
return Vec2f::Zero();
|
||||
return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]);
|
||||
};
|
||||
std::array<Vec2f, 3> uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])};
|
||||
obj_info.uvs.emplace_back(uv_array);
|
||||
}
|
||||
obj_info.face_colors.emplace_back(face_color);
|
||||
|
||||
@@ -51,19 +51,18 @@ static bool obj_parseline(const char *line, ObjData &data)
|
||||
line = endptr;
|
||||
EATWS();
|
||||
}
|
||||
/*double w = 0;
|
||||
// The optional w is accepted but not stored: only u and v are used.
|
||||
if (*line != 0) {
|
||||
w = strtod(line, &endptr);
|
||||
strtod(line, &endptr);
|
||||
if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0))
|
||||
return false;
|
||||
line = endptr;
|
||||
EATWS();
|
||||
}*/
|
||||
}
|
||||
if (*line != 0)
|
||||
return false;
|
||||
data.textureCoordinates.push_back((float)u);
|
||||
data.textureCoordinates.push_back((float)v);
|
||||
//data.textureCoordinates.push_back((float)w);
|
||||
break;
|
||||
}
|
||||
case 'n':
|
||||
@@ -245,7 +244,7 @@ static bool obj_parseline(const char *line, ObjData &data)
|
||||
else
|
||||
-- vertex.normalIdx;
|
||||
if (vertex.textureCoordIdx < 0)
|
||||
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / 3;
|
||||
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH;
|
||||
else
|
||||
-- vertex.textureCoordIdx;
|
||||
data.vertices.push_back(vertex);
|
||||
|
||||
@@ -92,6 +92,7 @@ inline bool operator==(const ObjSmoothingGroup &v1, const ObjSmoothingGroup &v2)
|
||||
}
|
||||
#define OBJ_VERTEX_COLOR_ALPHA 6
|
||||
#define OBJ_VERTEX_LENGTH 7 // x, y, z, color_x,color_y,color_z,color_w
|
||||
#define OBJ_TEXCOORD_LENGTH 2 // u, v
|
||||
#define ONE_FACE_SIZE 4//ONE_FACE format: f 8/4/6 7/3/6 6/2/6 -1/-1/-1
|
||||
struct ObjData {
|
||||
// Version of the data structure for load / store in the private binary format.
|
||||
@@ -100,7 +101,7 @@ struct ObjData {
|
||||
// x, y, z, color_x,color_y,color_z,color_w
|
||||
std::vector<float> coordinates;
|
||||
bool has_vertex_color{false};
|
||||
// u, v, w
|
||||
// u, v
|
||||
std::vector<float> textureCoordinates;
|
||||
// x, y, z
|
||||
std::vector<float> normals;
|
||||
|
||||
@@ -851,6 +851,8 @@ namespace client
|
||||
// If true, the macro processor will evaluate just a boolean condition using the full expressive power of the macro processor.
|
||||
bool just_boolean_expression = false;
|
||||
std::string error_message;
|
||||
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
|
||||
mutable std::set<std::string> inactive_local_variables;
|
||||
|
||||
// Table to translate symbol tag to a human readable error message.
|
||||
static std::map<std::string, std::string> tag_to_error_message;
|
||||
@@ -892,6 +894,8 @@ namespace client
|
||||
}
|
||||
// Inside a block, which is conditionally suppressed?
|
||||
bool skipping() const { return m_depth_suppressed > 0; }
|
||||
// Are variable names resolved inside the suppressed blocks too?
|
||||
bool check_inactive_names() const { return PlaceholderParser::check_inactive_branches && ! just_boolean_expression; }
|
||||
|
||||
const ConfigOption* optptr(const t_config_option_key &opt_key) const override
|
||||
{
|
||||
@@ -927,7 +931,7 @@ namespace client
|
||||
|
||||
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
|
||||
{
|
||||
if (ctx->skipping())
|
||||
if (ctx->skipping() && ! ctx->check_inactive_names())
|
||||
return;
|
||||
|
||||
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
||||
@@ -949,7 +953,9 @@ namespace client
|
||||
}
|
||||
}
|
||||
if (opt == nullptr)
|
||||
ctx->throw_exception("Variable does not exist", opt_key);
|
||||
ctx->throw_exception(ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist", opt_key);
|
||||
if (ctx->skipping())
|
||||
return;
|
||||
if (opt->is_scalar()) {
|
||||
if (opt->is_nil())
|
||||
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
|
||||
@@ -972,9 +978,10 @@ namespace client
|
||||
IteratorRange &opt_vector_index,
|
||||
std::string &output)
|
||||
{
|
||||
if (ctx->skipping())
|
||||
if (ctx->skipping() && ! ctx->check_inactive_names())
|
||||
return;
|
||||
|
||||
const char *not_found = ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist";
|
||||
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
||||
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
|
||||
if (opt == nullptr) {
|
||||
@@ -984,18 +991,20 @@ namespace client
|
||||
opt = ctx->resolve_symbol(opt_key_str);
|
||||
}
|
||||
if (opt == nullptr)
|
||||
ctx->throw_exception("Variable does not exist", opt_key);
|
||||
ctx->throw_exception(not_found, opt_key);
|
||||
}
|
||||
if (! opt->is_vector())
|
||||
ctx->throw_exception("Trying to index a scalar variable", opt_key);
|
||||
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
|
||||
if (opt_index == nullptr)
|
||||
ctx->throw_exception(not_found, opt_key);
|
||||
if (opt_index->type() != coInt)
|
||||
ctx->throw_exception("Indexing variable has to be integer", opt_key);
|
||||
if (ctx->skipping())
|
||||
return;
|
||||
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
|
||||
if (vec->empty())
|
||||
ctx->throw_exception("Indexing an empty vector variable", opt_key);
|
||||
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
|
||||
if (opt_index == nullptr)
|
||||
ctx->throw_exception("Variable does not exist", opt_key);
|
||||
if (opt_index->type() != coInt)
|
||||
ctx->throw_exception("Indexing variable has to be integer", opt_key);
|
||||
int idx = opt_index->getInt();
|
||||
if (idx < 0)
|
||||
ctx->throw_exception("Negative vector index", opt_key);
|
||||
@@ -1021,6 +1030,13 @@ namespace client
|
||||
output.writable = true;
|
||||
}
|
||||
output.opt = opt;
|
||||
} else if (ctx->check_inactive_names()) {
|
||||
// Only check the name. Back tracking may resolve the same identifier twice, so there are no side effects.
|
||||
const std::string key{ opt_key.begin(), opt_key.end() };
|
||||
if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr &&
|
||||
ctx->inactive_local_variables.count(key) == 0 &&
|
||||
(ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0))
|
||||
ctx->throw_exception("Not a variable name (in an inactive branch)", opt_key);
|
||||
}
|
||||
output.it_range = opt_key;
|
||||
}
|
||||
@@ -1426,6 +1442,13 @@ namespace client
|
||||
out.opt = ctx->config_local.optptr(key);
|
||||
}
|
||||
out.name = std::move(key);
|
||||
} else if (ctx->check_inactive_names()) {
|
||||
// Declared in a branch that is not taken: the name still counts as defined for the names that follow.
|
||||
std::string key(it_range.begin(), it_range.end());
|
||||
if (global_variable && ctx->context_data != nullptr)
|
||||
ctx->context_data->inactive_global_variables.insert(std::move(key));
|
||||
else
|
||||
ctx->inactive_local_variables.insert(std::move(key));
|
||||
}
|
||||
out.it_range = it_range;
|
||||
}
|
||||
@@ -1660,8 +1683,8 @@ namespace client
|
||||
const OptWithPos &rhs)
|
||||
{
|
||||
if (ctx->skipping())
|
||||
// Skipping, continue parsing.
|
||||
return true;
|
||||
// Skipping, let conditional_expression parse the whole right hand side, which may continue after the variable reference.
|
||||
return false;
|
||||
|
||||
if (lhs.opt) {
|
||||
assert(lhs.opt->is_vector());
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "libslic3r.h"
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -24,8 +25,15 @@ public:
|
||||
// If defined, then this dictionary is used by the scripts to define user variables and persist them
|
||||
// between PlaceholderParser evaluations.
|
||||
std::unique_ptr<DynamicConfig> global_config;
|
||||
// Global variables declared in {if} branches that were not taken, see check_inactive_branches.
|
||||
std::set<std::string> inactive_global_variables;
|
||||
};
|
||||
|
||||
// Orca: when set, variable names inside {if} branches that are not taken must resolve too, so a single
|
||||
// expansion checks every branch of a template. Only the profile validator's slice sweep sets it.
|
||||
// It does not apply to evaluate_boolean_expression(), where an error reads as "compatible".
|
||||
static inline bool check_inactive_branches = false;
|
||||
|
||||
PlaceholderParser(const DynamicConfig *external_config = nullptr);
|
||||
|
||||
void clear_config() { m_config.clear(); }
|
||||
|
||||
@@ -4896,7 +4896,8 @@ void MachineObject::update_slice_info(std::string project_id, std::string profil
|
||||
std::string subtask_json;
|
||||
unsigned http_code = 0;
|
||||
std::string http_body;
|
||||
if (m_agent->get_subtask_info(subtask_id, &subtask_json, &http_code, &http_body) == 0) {
|
||||
if (m_agent->get_subtask_info(subtask_id, &subtask_json, &http_code, &http_body,
|
||||
Slic3r::GUI::wxGetApp().get_printer_cloud_provider()) == 0) {
|
||||
try {
|
||||
if (!subtask_json.empty()) {
|
||||
|
||||
|
||||
@@ -345,16 +345,21 @@ boost::filesystem::path find_bundled_python_home()
|
||||
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
|
||||
if (valid_python_home(bundle_python))
|
||||
return bundle_python;
|
||||
#elif defined(_WIN32)
|
||||
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
||||
if (valid_python_home(exe_python))
|
||||
return exe_python;
|
||||
#else
|
||||
#elif !defined(_WIN32)
|
||||
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
|
||||
if (valid_python_home(linux_python))
|
||||
return linux_python;
|
||||
#endif
|
||||
|
||||
// Next to the executable: the Windows install layout, and the runtime copied
|
||||
// beside every platform's unit-test binary (tests/slic3rutils/CMakeLists.txt).
|
||||
// The CI test runner only receives the build/tests tree, so the candidates
|
||||
// below -- all of which point into the deps or install trees -- never resolve
|
||||
// there.
|
||||
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
||||
if (valid_python_home(exe_python))
|
||||
return exe_python;
|
||||
|
||||
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
|
||||
if (!configured_python.empty() && valid_python_home(configured_python))
|
||||
return configured_python;
|
||||
|
||||
@@ -16,6 +16,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_clipper_utils.cpp
|
||||
test_config.cpp
|
||||
test_config_variant_expansion.cpp
|
||||
test_drc.cpp
|
||||
test_toolordering_nozzle_group.cpp
|
||||
test_preset_bundle_loading.cpp
|
||||
test_preset_setting_id.cpp
|
||||
@@ -36,6 +37,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_mutable_priority_queue.cpp
|
||||
test_minimum_spanning_tree.cpp
|
||||
test_nozzle_volume_type.cpp
|
||||
test_obj.cpp
|
||||
test_step.cpp
|
||||
test_stl.cpp
|
||||
test_triangle_selector.cpp
|
||||
@@ -71,7 +73,9 @@ if (TARGET OpenVDB::openvdb)
|
||||
target_sources(${_TEST_NAME}_tests PRIVATE test_hollowing.cpp)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r Catch2::Catch2WithMain)
|
||||
# libslic3r links Draco privately; test_drc.cpp encodes its own fixtures.
|
||||
find_package(Draco REQUIRED)
|
||||
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r draco::draco Catch2::Catch2WithMain)
|
||||
target_include_directories(${_TEST_NAME}_tests PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/Format/DRC.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include <draco/compression/encode.h>
|
||||
#include <draco/mesh/mesh.h>
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// Encodes one triangle whose vertices carry a single attribute of the given type.
|
||||
// The decoder accepts both a mesh without POSITION and a face index past the point count.
|
||||
void write_drc_triangle(const std::string &path, draco::GeometryAttribute::Type attribute_type, uint32_t second_point)
|
||||
{
|
||||
draco::Mesh mesh;
|
||||
mesh.set_num_points(3);
|
||||
draco::GeometryAttribute attribute;
|
||||
attribute.Init(attribute_type, nullptr, 3, draco::DT_FLOAT32, false, sizeof(float) * 3, 0);
|
||||
const int attribute_id = mesh.AddAttribute(attribute, true, 3);
|
||||
const float points[3][3] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}};
|
||||
for (int i = 0; i < 3; ++i)
|
||||
mesh.attribute(attribute_id)->SetAttributeValue(draco::AttributeValueIndex(i), points[i]);
|
||||
draco::Mesh::Face face;
|
||||
face[0] = draco::PointIndex(0);
|
||||
face[1] = draco::PointIndex(second_point);
|
||||
face[2] = draco::PointIndex(2);
|
||||
mesh.AddFace(face);
|
||||
|
||||
draco::Encoder encoder;
|
||||
encoder.SetEncodingMethod(draco::MESH_SEQUENTIAL_ENCODING);
|
||||
draco::EncoderBuffer buffer;
|
||||
REQUIRE(encoder.EncodeMeshToBuffer(mesh, &buffer).ok());
|
||||
boost::nowide::ofstream out(path, std::ios::binary);
|
||||
out.write(buffer.data(), static_cast<std::streamsize>(buffer.size()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A Draco triangle with positions loads", "[DRC]")
|
||||
{
|
||||
ScopedTemporaryFile drc(".drc");
|
||||
write_drc_triangle(drc.string(), draco::GeometryAttribute::POSITION, 1);
|
||||
|
||||
TriangleMesh mesh;
|
||||
REQUIRE(load_drc(drc.string().c_str(), &mesh));
|
||||
CHECK(mesh.facets_count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("A Draco mesh without a POSITION attribute fails to load", "[DRC][Regression]")
|
||||
{
|
||||
ScopedTemporaryFile drc(".drc");
|
||||
write_drc_triangle(drc.string(), draco::GeometryAttribute::GENERIC, 1);
|
||||
|
||||
TriangleMesh mesh;
|
||||
CHECK_FALSE(load_drc(drc.string().c_str(), &mesh));
|
||||
}
|
||||
|
||||
TEST_CASE("A Draco face referencing a missing point fails to load", "[DRC][Regression]")
|
||||
{
|
||||
ScopedTemporaryFile drc(".drc");
|
||||
write_drc_triangle(drc.string(), draco::GeometryAttribute::POSITION, 200);
|
||||
|
||||
TriangleMesh mesh;
|
||||
CHECK_FALSE(load_drc(drc.string().c_str(), &mesh));
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Format/OBJ.hpp"
|
||||
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
namespace {
|
||||
|
||||
struct LoadedObj
|
||||
{
|
||||
bool ok{false};
|
||||
TriangleMesh mesh;
|
||||
ObjInfo info;
|
||||
};
|
||||
|
||||
// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1).
|
||||
// Only the first face and the vt lines are varied; the other three faces reference vt 1.
|
||||
LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n")
|
||||
{
|
||||
ScopedTemporaryFile obj(".obj");
|
||||
ScopedTemporaryFile mtl(".mtl");
|
||||
{
|
||||
boost::nowide::ofstream out(mtl.string());
|
||||
out << "newmtl a\nKd 1 0 0\n";
|
||||
}
|
||||
{
|
||||
boost::nowide::ofstream out(obj.string());
|
||||
out << "mtllib " << mtl.path().filename().string() << "\n"
|
||||
<< "v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n"
|
||||
<< vts
|
||||
<< "usemtl a\n"
|
||||
<< first_face << "\n"
|
||||
<< "f 1/1 2/1 4/1\nf 1/1 4/1 3/1\nf 2/1 3/1 4/1\n";
|
||||
}
|
||||
LoadedObj loaded;
|
||||
std::string message;
|
||||
loaded.ok = load_obj(obj.string().c_str(), &loaded.mesh, loaded.info, message);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("An out-of-range texture index falls back to a zero UV and keeps the geometry", "[OBJ][Regression]")
|
||||
{
|
||||
const LoadedObj loaded = load_textured_tetrahedron("f 1/1000000000 3/1 2/1");
|
||||
|
||||
REQUIRE(loaded.ok);
|
||||
CHECK(loaded.mesh.facets_count() == 4);
|
||||
REQUIRE(loaded.info.uvs.size() == 4);
|
||||
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||
CHECK_THAT(uv[0].x(), WithinAbs(0., 1e-6));
|
||||
CHECK_THAT(uv[0].y(), WithinAbs(0., 1e-6));
|
||||
CHECK_THAT(uv[1].x(), WithinAbs(0.25, 1e-6));
|
||||
CHECK_THAT(uv[1].y(), WithinAbs(0.5, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A face without texture indices loads among faces that have them", "[OBJ][Regression]")
|
||||
{
|
||||
const LoadedObj loaded = load_textured_tetrahedron("f 1 3 2");
|
||||
|
||||
REQUIRE(loaded.ok);
|
||||
CHECK(loaded.mesh.facets_count() == 4);
|
||||
// One UV entry per face, so later faces keep their own coordinates.
|
||||
REQUIRE(loaded.info.uvs.size() == 4);
|
||||
for (const Vec2f &uv : loaded.info.uvs.front()) {
|
||||
CHECK_THAT(uv.x(), WithinAbs(0., 1e-6));
|
||||
CHECK_THAT(uv.y(), WithinAbs(0., 1e-6));
|
||||
}
|
||||
CHECK_THAT(loaded.info.uvs[1][0].x(), WithinAbs(0.25, 1e-6));
|
||||
CHECK_THAT(loaded.info.uvs[1][0].y(), WithinAbs(0.5, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A negative texture index counts back from the last texture coordinate", "[OBJ]")
|
||||
{
|
||||
// -1 is the most recent vt (0.75, 1), -2 the one before it (0.25, 0.5).
|
||||
const LoadedObj loaded = load_textured_tetrahedron("f 1/-2 3/-1 2/-1");
|
||||
|
||||
REQUIRE(loaded.ok);
|
||||
CHECK(loaded.mesh.facets_count() == 4);
|
||||
REQUIRE(loaded.info.uvs.size() == 4);
|
||||
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||
CHECK_THAT(uv[0].x(), WithinAbs(0.25, 1e-6));
|
||||
CHECK_THAT(uv[0].y(), WithinAbs(0.5, 1e-6));
|
||||
CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6));
|
||||
CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Texture coordinates with a w component are kept", "[OBJ][Regression]")
|
||||
{
|
||||
const LoadedObj loaded = load_textured_tetrahedron("f 1/1 3/2 2/2", "vt 0.25 0.5 0\nvt 0.75 1 0\n");
|
||||
|
||||
REQUIRE(loaded.ok);
|
||||
CHECK(loaded.mesh.facets_count() == 4);
|
||||
REQUIRE(loaded.info.uvs.size() == 4);
|
||||
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||
CHECK_THAT(uv[0].x(), WithinAbs(0.25, 1e-6));
|
||||
CHECK_THAT(uv[0].y(), WithinAbs(0.5, 1e-6));
|
||||
CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6));
|
||||
CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A texture coordinate with w does not shift the indices of the ones after it", "[OBJ][Regression]")
|
||||
{
|
||||
// The w on the first vt used to drop that line, so vt 2 resolved to the third coordinate.
|
||||
const LoadedObj loaded = load_textured_tetrahedron("f 1/2 3/3 2/-1", "vt 0.1 0.2 0\nvt 0.25 0.5\nvt 0.75 1\n");
|
||||
|
||||
REQUIRE(loaded.ok);
|
||||
REQUIRE(loaded.info.uvs.size() == 4);
|
||||
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||
CHECK_THAT(uv[0].x(), WithinAbs(0.25, 1e-6));
|
||||
CHECK_THAT(uv[0].y(), WithinAbs(0.5, 1e-6));
|
||||
CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6));
|
||||
CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6));
|
||||
CHECK_THAT(uv[2].x(), WithinAbs(0.75, 1e-6));
|
||||
CHECK_THAT(uv[2].y(), WithinAbs(1., 1e-6));
|
||||
}
|
||||
@@ -356,3 +356,38 @@ SCENARIO("Placeholder parser coFloatsOrPercents vector access", "[PlaceholderPar
|
||||
REQUIRE(std::stod(parser.process("{pressure_advance[2]}")) == Catch::Approx(3.0));
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Placeholder parser names in branches that are not taken", "[PlaceholderParser]") {
|
||||
PlaceholderParser parser;
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
parser.apply_config(config);
|
||||
parser.set("idx", 0);
|
||||
PlaceholderParser::ContextData context;
|
||||
context.global_config = std::make_unique<DynamicConfig>();
|
||||
auto process = [&parser, &context](const std::string &templ) { return parser.process(templ, 0, nullptr, nullptr, &context); };
|
||||
|
||||
SECTION("a declaration continuing after a variable reference parses when not taken") {
|
||||
REQUIRE(process("{if false}{local a = layer_height + 1}{endif}ok") == "ok");
|
||||
}
|
||||
SECTION("names are not checked by default") {
|
||||
REQUIRE(process("{if false}{no_such_var}[no_such_var]{endif}ok") == "ok");
|
||||
}
|
||||
SECTION("names must resolve when check_inactive_branches is set") {
|
||||
struct Restore { ~Restore() { PlaceholderParser::check_inactive_branches = false; } } restore;
|
||||
PlaceholderParser::check_inactive_branches = true;
|
||||
|
||||
CHECK_THROWS_WITH(process("{if false}{no_such_var}{endif}"), Catch::Matchers::ContainsSubstring("Not a variable name (in an inactive branch)"));
|
||||
CHECK_THROWS_WITH(process("{if false}[no_such_var]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)"));
|
||||
CHECK_THROWS_WITH(process("{if false}[nozzle_temperature[no_such_var]]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)"));
|
||||
CHECK_THROWS(process("{if true}{else}{no_such_var}{endif}"));
|
||||
CHECK_THROWS(process("{if false}{local a = no_such_var + 1}{endif}"));
|
||||
|
||||
CHECK(process("{if false}{layer_height}[layer_height][nozzle_temperature_0][nozzle_temperature[idx]]{endif}ok") == "ok");
|
||||
CHECK(process("{if false}{local a = 1}{a = a + 1}{a}{endif}{if false}{a}{endif}ok") == "ok");
|
||||
// A global declared in a branch that is not taken counts as defined for later expansions sharing the context.
|
||||
CHECK(process("{if false}{global g = 1}{endif}{if false}{g}{endif}ok") == "ok");
|
||||
CHECK(process("{if false}{g}{endif}ok") == "ok");
|
||||
// Boolean expressions are not checked, so compatibility conditions keep their behaviour.
|
||||
CHECK(PlaceholderParser::evaluate_boolean_expression("false ? no_such_var == 1 : true", config));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,16 @@ if (WIN32)
|
||||
COMMENT "Copying Python runtime for slic3rutils plugin host API tests"
|
||||
VERBATIM
|
||||
)
|
||||
elseif (APPLE)
|
||||
target_link_options(${_TEST_NAME}_tests PRIVATE
|
||||
"LINKER:-rpath,@executable_path/python/lib")
|
||||
elseif (NOT FLATPAK)
|
||||
# The CI unit-test runner only receives the build/tests tree, so both the
|
||||
# interpreter and the libpython the test binary links have to travel next to
|
||||
# the executable; find_bundled_python_home() picks the copy up from there.
|
||||
if (APPLE)
|
||||
target_link_options(${_TEST_NAME}_tests PRIVATE
|
||||
"LINKER:-rpath,@executable_path/python/lib")
|
||||
else ()
|
||||
set_property(TARGET ${_TEST_NAME}_tests APPEND PROPERTY BUILD_RPATH "$ORIGIN/python/lib")
|
||||
endif ()
|
||||
|
||||
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf
|
||||
@@ -61,7 +68,7 @@ elseif (APPLE)
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_PREFIX_PATH}/libpython"
|
||||
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
|
||||
COMMENT "Copying Python runtime for macOS plugin host API tests"
|
||||
COMMENT "Copying Python runtime for the plugin host API tests"
|
||||
VERBATIM
|
||||
)
|
||||
elseif (FLATPAK)
|
||||
@@ -78,4 +85,16 @@ elseif (FLATPAK)
|
||||
)
|
||||
endif()
|
||||
|
||||
# scripts/run_unit_tests.sh installs the tests' numpy into that runtime with this uv,
|
||||
# staged where the app build tree keeps it (<exe dir>/tools/uv, see src/CMakeLists.txt).
|
||||
if (ORCA_BUNDLED_UV_EXECUTABLE)
|
||||
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/tools/uv"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ORCA_BUNDLED_UV_EXECUTABLE}"
|
||||
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/tools/uv/${ORCA_BUNDLED_UV_FILENAME}"
|
||||
COMMENT "Copying uv for the plugin host API tests"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
Reference in New Issue
Block a user