mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-09 08:41:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24862cc3bf | ||
|
|
48d7f23f9f | ||
|
|
6d616c288a | ||
|
|
bff19b07d2 | ||
|
|
25ec403156 | ||
|
|
018c4f49ee | ||
|
|
4c74d84b94 | ||
|
|
65978fc87a | ||
|
|
9a9285a894 | ||
|
|
ae59656fa4 | ||
|
|
627ae12dd3 | ||
|
|
b3490a1cdc | ||
|
|
c7e7fa2a0b | ||
|
|
16380e8560 | ||
|
|
785a1946a6 | ||
|
|
8790b07773 | ||
|
|
1ec9b315f5 | ||
|
|
e6be22dd4c | ||
|
|
ee2c40ea85 | ||
|
|
6d6d8e8b46 | ||
|
|
f3385c1e03 | ||
|
|
9553326e7d |
+2
-2
@@ -1084,10 +1084,10 @@ endif ()
|
||||
|
||||
find_path(SPNAV_INCLUDE_DIR spnav.h)
|
||||
if (SPNAV_INCLUDE_DIR)
|
||||
find_library(SPNAV_LIB NAMES libspnav.a) # Force linking libspnav statically
|
||||
find_library(SPNAV_LIB NAMES libspnav.a spnav)
|
||||
if (SPNAV_LIB)
|
||||
add_definitions(-DHAVE_SPNAV)
|
||||
message(STATUS "SPNAV library found")
|
||||
message(STATUS "SPNAV library found: ${SPNAV_LIB}")
|
||||
else()
|
||||
message(STATUS "SPNAV library NOT found, Spacenavd not supported")
|
||||
endif()
|
||||
|
||||
@@ -104,8 +104,8 @@ fi
|
||||
|
||||
CMAKE_VERSION=$(cmake --version | head -1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
|
||||
if [ "$CMAKE_VERSION" -ge 4 ] 2>/dev/null; then
|
||||
export CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
|
||||
export CMAKE_POLICY_VERSION_MINIMUM=3.10
|
||||
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.10"
|
||||
echo "Detected CMake 4.x, adding compatibility flag (env + cmake arg)"
|
||||
else
|
||||
export CMAKE_POLICY_COMPAT=""
|
||||
|
||||
Vendored
+4
-4
@@ -1,5 +1,5 @@
|
||||
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0")
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "" FORCE)
|
||||
endif()
|
||||
|
||||
#
|
||||
@@ -24,7 +24,7 @@ endif()
|
||||
# therefore, unfortunately, the installation cannot be copied/moved elsewhere without re-installing wxWidgets.
|
||||
#
|
||||
|
||||
cmake_minimum_required(VERSION 3.2)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
if (APPLE)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
|
||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
@@ -224,7 +224,7 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
||||
${_source_dir_arg}
|
||||
${_gen}
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
|
||||
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
|
||||
-DCMAKE_MODULE_PATH:STRING=${PROJECT_SOURCE_DIR}/../cmake/modules
|
||||
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
|
||||
@@ -279,7 +279,7 @@ else()
|
||||
${_source_dir_arg}
|
||||
${_gen}
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
|
||||
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
|
||||
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
|
||||
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
diff --git a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
|
||||
index 6f63781..9b1c08e 100644
|
||||
--- a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
|
||||
+++ b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
|
||||
@@ -210,10 +210,10 @@ private:
|
||||
|
||||
// Define two pcurves of the seam-edge.
|
||||
occ::handle<Geom2d_Curve> aPC1, aPC2;
|
||||
- double af, al;
|
||||
+ double af, al, af1, al1;
|
||||
|
||||
aE.Orientation(TopAbs_FORWARD);
|
||||
- aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
|
||||
+ aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af1, al1);
|
||||
|
||||
aE.Orientation(TopAbs_REVERSED);
|
||||
aPC2 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
|
||||
@@ -224,7 +224,9 @@ private:
|
||||
}
|
||||
|
||||
// Select the correct pcurve of the seam-edge.
|
||||
- const gp_Pnt2d& aFPntOfPC1 = aPC1->Value(aPC1->FirstParameter());
|
||||
+ // Use the edge's first parameter. A Geom2d_Line's FirstParameter() is -Precision::Infinite(),
|
||||
+ // where a direction of (2e-16, -1) from rounding error gives an X far outside the U range.
|
||||
+ const gp_Pnt2d aFPntOfPC1 = aPC1->Value(af1);
|
||||
|
||||
if (std::abs(aLPntOfIPC1.X() - aFPntOfPC1.X()) > Precision::Confusion())
|
||||
{
|
||||
Vendored
+7
@@ -25,9 +25,16 @@ endif()
|
||||
# shipped bytes. Windows ships only the DLLs libslic3r links, so the tab adds the TKFillet,
|
||||
# TKOffset and TKBool DLLs. See docs/HLSD/design-tab.md.
|
||||
|
||||
if (IN_GIT_REPO)
|
||||
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(OCCT
|
||||
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V8_0_1.zip
|
||||
URL_HASH SHA256=7c033d917ee8f040c0512d289dcc5f02c148889d5bac17c3e25639accb44f0da
|
||||
# Makes BRepMesh triangulate cone faces whose seam pcurve is slightly tilted
|
||||
# (Open-Cascade-SAS/OCCT#572); remove the patch once an OCCT release includes the fix.
|
||||
PATCH_COMMAND git apply ${OCCT_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
|
||||
#DEPENDS dep_Boost
|
||||
DEPENDS ${FREETYPE_PKG}
|
||||
CMAKE_ARGS
|
||||
|
||||
@@ -79,6 +79,13 @@ the same `infill_body()`. The octree of the whole object is built only for an
|
||||
object of a single body, or when some body received no triangles, which then
|
||||
uses it.
|
||||
|
||||
The line spacing of an octree comes from the density, line width and multiline
|
||||
count of a region, so a modifier or a part with its own density needs octrees of
|
||||
its own. `adaptive_fill_line_spacing()` gives the spacing of each region, and
|
||||
`FillAdaptive::RegionOctrees` holds one set of octrees per distinct spacing,
|
||||
shared by the regions that have it. A set is built only for the bodies its
|
||||
regions fill. The fill takes the set of its region, then the octree of its body.
|
||||
|
||||
## Patterns left out
|
||||
|
||||
Lightning grows its trees over the whole object, so moving a reference point
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><circle cx="9.5" cy="12.5" r="9.5" style="fill:#009688"/><path d="M6.5,10.5a3,3,0,0,1,6,0c0,3-3,2-3,4v1" style="fill:none;stroke:#e6e6e6;stroke-linecap:round;stroke-linejoin:round"/><rect x="9" y="17" width="1" height="1" style="fill:#e6e6e6"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 333 B After Width: | Height: | Size: 366 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><circle cx="9.5" cy="12.5" r="9.5" style="fill:#33aba1"/><path d="M6.5,10.5a3,3,0,0,1,6,0c0,3-3,2-3,4v1" style="fill:none;stroke:#e6e6e6;stroke-linecap:round;stroke-linejoin:round"/><rect x="9" y="17" width="1" height="1" style="fill:#e6e6e6"/></svg>
|
||||
|
Before Width: | Height: | Size: 333 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M12.31,8l-8.14,4.16c-.31.16-.67-.07-.67-.41V3.27c0-.35.37-.57.68-.41l8.14,4.32c.33.17.33.65,0,.82Z" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M7.5,3.5c2-2,5.1-1,6-1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M7.5,12.5c2-2,5.1-1,6-1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M7.5,3.5C5.5,1.5,2.4,2.5,1.5,2.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M7.5,12.5c-2-2-5.1-1-6-1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><line x1="13.5" y1="11.5" x2="13.5" y2="2.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><line x1="7.5" y1="12.5" x2="7.5" y2="3.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><line x1="1.5" y1="11.5" x2="1.5" y2="2.5" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
After Width: | Height: | Size: 954 B |
+1
-1
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@ RUN set -eux; \
|
||||
tar -xzf /tmp/assimp.tar.gz -C /tmp/assimp-src --strip-components=1; \
|
||||
DESTDIR=/OrcaSlicer/deps/build/destdir/usr/local; \
|
||||
cmake -S /tmp/assimp-src -B /tmp/assimp-build -G Ninja \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.10 \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="$DESTDIR" \
|
||||
-DCMAKE_PREFIX_PATH="$DESTDIR" \
|
||||
|
||||
+6
-2
@@ -1820,6 +1820,8 @@ int CLI::run(int argc, char **argv)
|
||||
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
|
||||
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
|
||||
}
|
||||
// A 3mf can carry an empty project_settings.config - the models in
|
||||
// resources/handy_models do - and opt_float() dereferences without checking.
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
old_printable_height = (int)(config.opt_float("printable_height"));
|
||||
|
||||
@@ -2505,7 +2507,8 @@ int CLI::run(int argc, char **argv)
|
||||
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
|
||||
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
|
||||
}
|
||||
orig_printable_height = (int)(config.opt_float("printable_height"));
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
orig_printable_height = (int)(config.opt_float("printable_height"));
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
|
||||
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
|
||||
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
|
||||
@@ -4617,7 +4620,8 @@ int CLI::run(int argc, char **argv)
|
||||
BoundingBoxf temp_printable_bbox(temp_printable_area);
|
||||
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
|
||||
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
|
||||
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
|
||||
}
|
||||
if (temp_exclude_area.size() >= 4) {
|
||||
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
|
||||
|
||||
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
|
||||
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
|
||||
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
|
||||
# chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
|
||||
add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
|
||||
target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
|
||||
target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# profile_include_dump: prints what included templates contribute to a vendor's presets,
|
||||
# to diff against the same tool built in BambuStudio. Built only on request.
|
||||
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
|
||||
|
||||
@@ -303,10 +303,11 @@ else
|
||||
fi
|
||||
|
||||
has_host_runtime_library() {
|
||||
local lib_name path
|
||||
local lib_name="\$1"
|
||||
local path
|
||||
|
||||
if command -v ldconfig >/dev/null 2>&1; then
|
||||
if ldconfig -p 2>/dev/null | grep -Fq " \$lib_name"; then
|
||||
if ldconfig -p 2>/dev/null | grep -Fq "\$lib_name ("; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
// Diagnostic for the LSCM unwrap of a texture displacement layer.
|
||||
//
|
||||
// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built
|
||||
// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand,
|
||||
// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses.
|
||||
// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap,
|
||||
// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at.
|
||||
//
|
||||
// texture_unwrap_dump <project.3mf>
|
||||
|
||||
// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation,
|
||||
// so every executable that links libslic3r has to supply it. Must precede any include that pulls the
|
||||
// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools.
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvg.h"
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t edge_key(int a, int b)
|
||||
{
|
||||
if (a > b)
|
||||
std::swap(a, b);
|
||||
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
||||
}
|
||||
|
||||
// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the
|
||||
// topological disk LSCM needs (one loop, V - E + F == 1).
|
||||
void chart_topology(const indexed_triangle_set &mesh, const std::vector<int> &faces, int &loops, int &euler)
|
||||
{
|
||||
std::unordered_map<uint64_t, int> edge_use;
|
||||
std::unordered_map<int, int> local;
|
||||
for (const int f : faces) {
|
||||
const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
++edge_use[edge_key(t[i], t[(i + 1) % 3])];
|
||||
local.emplace(t[i], int(local.size()));
|
||||
}
|
||||
}
|
||||
euler = int(local.size()) - int(edge_use.size()) + int(faces.size());
|
||||
|
||||
std::unordered_map<int, int> parent;
|
||||
const std::function<int(int)> find = [&](int x) {
|
||||
while (parent[x] != x)
|
||||
x = parent[x] = parent[parent[x]];
|
||||
return x;
|
||||
};
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1)
|
||||
for (const int v : { int(key >> 32), int(uint32_t(key)) })
|
||||
parent.emplace(v, v);
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1) {
|
||||
const int a = find(int(key >> 32)), b = find(int(uint32_t(key)));
|
||||
if (a != b)
|
||||
parent[b] = a;
|
||||
}
|
||||
std::unordered_map<int, int> roots;
|
||||
for (const auto &[v, p] : parent)
|
||||
roots[find(v)] = 1;
|
||||
loops = int(roots.size());
|
||||
}
|
||||
|
||||
float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c)
|
||||
{
|
||||
return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
std::printf("usage: texture_unwrap_dump <project.3mf>\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
Model model;
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
PlateDataPtrs plate_data;
|
||||
std::vector<Preset *> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
// The importer writes a backup copy under the data dir and silently loses objects without one.
|
||||
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump";
|
||||
boost::filesystem::create_directories(tmp);
|
||||
set_data_dir(tmp.string());
|
||||
|
||||
// LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is
|
||||
// dropped by the plate mapping, which is what "skip this object" in the log means.
|
||||
if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf,
|
||||
&file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances |
|
||||
LoadStrategy::Silence)) {
|
||||
std::printf("failed to load %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::printf("loaded: %zu object(s)\n", model.objects.size());
|
||||
|
||||
for (const ModelObject *object : model.objects)
|
||||
for (const ModelVolume *volume : object->volumes) {
|
||||
if (volume->texture_displacement_layers.empty()) {
|
||||
std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:",
|
||||
volume->name.c_str());
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size());
|
||||
std::printf("\n");
|
||||
continue;
|
||||
}
|
||||
std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(),
|
||||
volume->mesh().its.indices.size(), volume->texture_displacement_layers.size());
|
||||
|
||||
for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) {
|
||||
std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n",
|
||||
layer.slot, layer.name.c_str(), int(layer.projection_method),
|
||||
layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size());
|
||||
if (layer.projection_method != TextureProjectionMethod::LSCM)
|
||||
continue;
|
||||
|
||||
const indexed_triangle_set patch =
|
||||
extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data());
|
||||
std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size());
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f,
|
||||
layer.lscm_seam_edges);
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_patch_unwrap: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t1 - t0).count());
|
||||
std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count,
|
||||
unwrap.indices.size());
|
||||
|
||||
// Group the patch's faces by chart so each can be examined on its own.
|
||||
std::vector<std::vector<int>> chart_faces(size_t(std::max(unwrap.chart_count, 0)));
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
if (chart >= 0 && size_t(chart) < chart_faces.size())
|
||||
chart_faces[size_t(chart)].push_back(unwrap.source_face[i]);
|
||||
}
|
||||
|
||||
int bad_charts = 0;
|
||||
for (size_t c = 0; c < chart_faces.size(); ++c) {
|
||||
int loops = 0, euler = 0;
|
||||
chart_topology(patch, chart_faces[c], loops, euler);
|
||||
|
||||
// Flipped triangles: the unwrap folded over itself, which is what a planar fallback
|
||||
// does to a chart that is not flat. Measured on the unwrap's own triangles.
|
||||
int pos = 0, neg = 0;
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const stl_triangle_vertex_indices &t = unwrap.indices[i];
|
||||
if (unwrap.vertex_chart[size_t(t[0])] != int(c))
|
||||
continue;
|
||||
const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])],
|
||||
unwrap.uvs[size_t(t[2])]);
|
||||
if (a > 0.f) ++pos; else if (a < 0.f) ++neg;
|
||||
}
|
||||
const int flipped = std::min(pos, neg);
|
||||
const bool disk = loops == 1 && euler == 1;
|
||||
if (!disk || flipped > 0) {
|
||||
++bad_charts;
|
||||
std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c,
|
||||
chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped,
|
||||
pos + neg, flipped ? " FOLDED" : "");
|
||||
}
|
||||
}
|
||||
std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count);
|
||||
|
||||
// What the eye actually sees. Every patch edge shared by two charts should carry the same
|
||||
// UV on both sides once the islands are laid out as a connected net; where it does not,
|
||||
// the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact
|
||||
// coordinates the bake and the checker overlay sample.
|
||||
{
|
||||
const auto n0 = std::chrono::steady_clock::now();
|
||||
const std::vector<TextureIsland> net = compute_connected_net(unwrap);
|
||||
const auto n1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n",
|
||||
std::chrono::duration<double, std::milli>(n1 - n0).count(), net.size());
|
||||
}
|
||||
const auto t2 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> uv = compute_lscm_uvs(patch, layer);
|
||||
const auto t3 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n",
|
||||
std::chrono::duration<double, std::milli>(t3 - t2).count());
|
||||
if (uv.size() != patch.vertices.size()) {
|
||||
std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(),
|
||||
patch.vertices.size());
|
||||
continue;
|
||||
}
|
||||
// Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows
|
||||
// the jump directly: the same mesh vertex lands at two different UVs. That is exactly what
|
||||
// the eye reads as the texture breaking.
|
||||
const auto t4 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> corner = compute_lscm_corner_uvs(patch, layer);
|
||||
const auto t5 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t5 - t4).count());
|
||||
// Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered*
|
||||
// endpoint. Comparing that same vertex on both sides is the point: indexing by corner
|
||||
// position instead compares opposite ends of the edge, because the two faces wind it in
|
||||
// opposite directions.
|
||||
std::unordered_map<uint64_t, std::vector<Vec2f>> edge_seen;
|
||||
if (corner.size() == patch.indices.size() * 3)
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const int a = t[k], b = t[(k + 1) % 3];
|
||||
const int probe = std::min(a, b);
|
||||
const int local = (a == probe) ? k : (k + 1) % 3;
|
||||
edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]);
|
||||
}
|
||||
}
|
||||
// Which chart each patch face belongs to, so a broken edge can be attributed to a pair.
|
||||
std::vector<int> chart_of_face(patch.indices.size(), -1);
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i)
|
||||
chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
|
||||
std::unordered_map<uint64_t, std::vector<int>> edge_faces;
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f));
|
||||
}
|
||||
|
||||
int adjacent = 0, broken = 0, broken_same_chart = 0;
|
||||
float worst = 0.f;
|
||||
std::map<std::pair<int, int>, std::pair<int, float>> by_pair;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2)
|
||||
continue;
|
||||
++adjacent;
|
||||
const float d = (seen[0] - seen[1]).norm();
|
||||
if (d <= 1e-4f)
|
||||
continue;
|
||||
++broken;
|
||||
worst = std::max(worst, d);
|
||||
const auto &faces_here = edge_faces[key];
|
||||
int c1 = -1, c2 = -1;
|
||||
if (faces_here.size() == 2) {
|
||||
c1 = chart_of_face[size_t(faces_here[0])];
|
||||
c2 = chart_of_face[size_t(faces_here[1])];
|
||||
}
|
||||
if (c1 == c2)
|
||||
++broken_same_chart;
|
||||
auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }];
|
||||
++slot.first;
|
||||
slot.second = std::max(slot.second, d);
|
||||
}
|
||||
std::printf(" broken edges inside a single chart: %d\n", broken_same_chart);
|
||||
std::printf(" broken by chart pair:");
|
||||
for (const auto &[pk, v] : by_pair)
|
||||
std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second);
|
||||
std::printf("\n");
|
||||
// Total length of the seams left broken, in mm: how much visibly torn edge the layout has,
|
||||
// which is what the eye adds up. A count alone hides whether the breaks are hairlines or
|
||||
// whole sides of an island.
|
||||
float seam_mm = 0.f;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f)
|
||||
continue;
|
||||
seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm();
|
||||
}
|
||||
std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n",
|
||||
adjacent, broken, seam_mm, worst);
|
||||
std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(),
|
||||
unwrap.chart_count,
|
||||
layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used"
|
||||
: "net rebuilt");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1320,7 +1320,7 @@ void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill>
|
||||
#endif
|
||||
|
||||
// friend to Layer
|
||||
void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator)
|
||||
void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator)
|
||||
{
|
||||
for (LayerRegion *layerm : m_regions)
|
||||
layerm->fills.clear();
|
||||
@@ -1353,7 +1353,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const
|
||||
f->z = this->print_z;
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
if (surface_fill.params.pattern == ipConcentricInternal) {
|
||||
@@ -1516,7 +1516,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const
|
||||
* - For lightning/adaptive patterns, the respective generators are wired so their
|
||||
* polylines match the final infill layout.
|
||||
*/
|
||||
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) const
|
||||
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator) const
|
||||
{
|
||||
LockRegionParam skin_inner_param;
|
||||
std::vector<SurfaceFill> surface_fills = group_fills(*this, skin_inner_param);
|
||||
@@ -1574,7 +1574,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
|
||||
f->z = this->print_z;
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
|
||||
|
||||
@@ -301,88 +301,25 @@ void OctreeDeleter::operator()(Octree *p) {
|
||||
delete p;
|
||||
}
|
||||
|
||||
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object)
|
||||
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object)
|
||||
{
|
||||
// Output, spacing for icAdaptiveCubic and icSupportCubic
|
||||
double adaptive_line_spacing = 0.;
|
||||
double support_line_spacing = 0.;
|
||||
|
||||
enum class Tristate {
|
||||
Yes,
|
||||
No,
|
||||
Maybe
|
||||
};
|
||||
struct RegionFillData {
|
||||
Tristate has_adaptive_infill;
|
||||
Tristate has_support_infill;
|
||||
double density;
|
||||
double extrusion_width;
|
||||
};
|
||||
std::vector<RegionFillData> region_fill_data;
|
||||
region_fill_data.reserve(print_object.num_printing_regions());
|
||||
bool build_octree = false;
|
||||
std::vector<double> line_spacing(print_object.num_printing_regions(), 0.);
|
||||
const std::vector<double> &nozzle_diameters = print_object.print()->config().nozzle_diameter.values;
|
||||
double max_nozzle_diameter = *std::max_element(nozzle_diameters.begin(), nozzle_diameters.end());
|
||||
double default_infill_extrusion_width = Flow::auto_extrusion_width(FlowRole::frInfill, float(max_nozzle_diameter));
|
||||
for (size_t region_id = 0; region_id < print_object.num_printing_regions(); ++ region_id) {
|
||||
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
|
||||
bool nonempty = config.sparse_infill_density > 0;
|
||||
bool has_adaptive_infill = nonempty && config.sparse_infill_pattern == ipAdaptiveCubic;
|
||||
bool has_support_infill = nonempty && config.sparse_infill_pattern == ipSupportCubic;
|
||||
double sparse_infill_line_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
|
||||
region_fill_data.push_back(RegionFillData({
|
||||
has_adaptive_infill ? Tristate::Maybe : Tristate::No,
|
||||
has_support_infill ? Tristate::Maybe : Tristate::No,
|
||||
config.sparse_infill_density,
|
||||
sparse_infill_line_width != 0. ? sparse_infill_line_width : default_infill_extrusion_width
|
||||
}));
|
||||
build_octree |= has_adaptive_infill || has_support_infill;
|
||||
for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id) {
|
||||
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
|
||||
if (config.sparse_infill_density <= 0 || ! is_octree_infill_pattern(config.sparse_infill_pattern) ||
|
||||
std::none_of(print_object.layers().begin(), print_object.layers().end(), [region_id](const Layer *layer) {
|
||||
return region_id < layer->regions().size() && ! layer->regions()[region_id]->fill_surfaces.empty();
|
||||
}))
|
||||
continue;
|
||||
double extrusion_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
|
||||
if (extrusion_width == 0.)
|
||||
extrusion_width = default_infill_extrusion_width;
|
||||
line_spacing[region_id] = extrusion_width / ((config.sparse_infill_density / 100.0f) * 0.333333333f) * config.fill_multiline.value;
|
||||
}
|
||||
|
||||
if (build_octree) {
|
||||
// Compute the average of above parameters over all layers
|
||||
for (const Layer *layer : print_object.layers())
|
||||
for (size_t region_id = 0; region_id < layer->regions().size(); ++ region_id) {
|
||||
RegionFillData &rd = region_fill_data[region_id];
|
||||
if (rd.has_adaptive_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
|
||||
rd.has_adaptive_infill = Tristate::Yes;
|
||||
if (rd.has_support_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
|
||||
rd.has_support_infill = Tristate::Yes;
|
||||
}
|
||||
|
||||
double adaptive_fill_density = 0.;
|
||||
double adaptive_infill_extrusion_width = 0.;
|
||||
int adaptive_cnt = 0;
|
||||
double support_fill_density = 0.;
|
||||
double support_infill_extrusion_width = 0.;
|
||||
int support_cnt = 0;
|
||||
|
||||
for (const RegionFillData &rd : region_fill_data) {
|
||||
if (rd.has_adaptive_infill == Tristate::Yes) {
|
||||
adaptive_fill_density += rd.density;
|
||||
adaptive_infill_extrusion_width += rd.extrusion_width;
|
||||
++ adaptive_cnt;
|
||||
} else if (rd.has_support_infill == Tristate::Yes) {
|
||||
support_fill_density += rd.density;
|
||||
support_infill_extrusion_width += rd.extrusion_width;
|
||||
++ support_cnt;
|
||||
}
|
||||
}
|
||||
|
||||
auto to_line_spacing = [](int cnt, double density, double extrusion_width) {
|
||||
if (cnt) {
|
||||
density /= double(cnt);
|
||||
extrusion_width /= double(cnt);
|
||||
return extrusion_width / ((density / 100.0f) * 0.333333333f);
|
||||
} else
|
||||
return 0.;
|
||||
};
|
||||
const int n_multiline = print_object.printing_region(0).config().fill_multiline.value;
|
||||
adaptive_line_spacing = to_line_spacing(adaptive_cnt, adaptive_fill_density, adaptive_infill_extrusion_width) * n_multiline;
|
||||
support_line_spacing = to_line_spacing(support_cnt, support_fill_density, support_infill_extrusion_width) * n_multiline;
|
||||
}
|
||||
|
||||
return std::make_pair(adaptive_line_spacing, support_line_spacing);
|
||||
return line_spacing;
|
||||
}
|
||||
|
||||
// Context used by generate_infill_lines() when recursively traversing an octree in a DDA fashion
|
||||
|
||||
@@ -59,11 +59,22 @@ struct Octrees
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate line spacing for
|
||||
// 1) adaptive cubic infill
|
||||
// 2) adaptive internal support cubic infill
|
||||
// Returns zero for a particular infill type if no such infill is to be generated.
|
||||
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object);
|
||||
// Orca: The octrees of each line spacing the regions of an object fill with.
|
||||
struct RegionOctrees
|
||||
{
|
||||
std::vector<Octrees> sets;
|
||||
// Index into sets for each region, -1 for a region without adaptive or support cubic infill.
|
||||
std::vector<int> region_set;
|
||||
|
||||
const Octrees *region(size_t region_id) const
|
||||
{
|
||||
return region_id < region_set.size() && region_set[region_id] >= 0 ? &sets[region_set[region_id]] : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Line spacing of the adaptive or support cubic infill of each region of the object,
|
||||
// zero for a region that generates no such infill.
|
||||
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object);
|
||||
|
||||
// Rotation of the octree to stand on one of its corners.
|
||||
Eigen::Quaterniond transform_to_world();
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <deque>
|
||||
@@ -2649,6 +2650,43 @@ void GCodeProcessorResult::reset() {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: this=%2% reset finished")%__LINE__%this;
|
||||
}
|
||||
|
||||
bool GCodeProcessorResult::rebuild_lines_ends()
|
||||
{
|
||||
// lock_guard rather than the lock()/unlock() pair reset() uses: this reads a file and grows a
|
||||
// vector, so a throw between the two would leave the mutex held forever.
|
||||
std::lock_guard<std::mutex> lock(result_mutex);
|
||||
// A partially rebuilt map would have the G-code window slicing lines at wrong offsets all over
|
||||
// again, so every failure below leaves lines_ends empty, which just hides the window.
|
||||
lines_ends.clear();
|
||||
|
||||
FilePtr in{ boost::nowide::fopen(filename.c_str(), "rb") };
|
||||
if (in.f == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": cannot open " << filename << " to rebuild the G-code line offsets.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Record the offset one past each '\n', matching what ExportLines emits during the export pass.
|
||||
std::vector<char> buffer(65536, 0);
|
||||
size_t file_pos = 0;
|
||||
for (;;) {
|
||||
const size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f);
|
||||
if (::ferror(in.f)) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": error while reading " << filename
|
||||
<< " to rebuild the G-code line offsets.";
|
||||
lines_ends.clear();
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < cnt_read; ++i) {
|
||||
if (buffer[i] == '\n')
|
||||
lines_ends.emplace_back(file_pos + i + 1);
|
||||
}
|
||||
file_pos += cnt_read;
|
||||
if (cnt_read < buffer.size())
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<std::pair<GCodeProcessor::EProducer, std::string>> GCodeProcessor::Producers = {
|
||||
//BBS: OrcaSlicer is also "bambu". Otherwise the time estimation didn't work.
|
||||
//FIXME: Workaround and should be handled when do removing-bambu
|
||||
|
||||
@@ -334,6 +334,11 @@ class Print;
|
||||
|
||||
BedType bed_type = BedType::btCount;
|
||||
void reset();
|
||||
// Re-scan this->filename and rebuild lines_ends from it. Needed whenever the exported G-code is
|
||||
// modified in place after export (post-processing scripts), which shifts the byte offsets the
|
||||
// G-code viewer uses to slice lines out of the memory-mapped file. Returns false and leaves
|
||||
// lines_ends empty if the file cannot be read.
|
||||
bool rebuild_lines_ends();
|
||||
|
||||
//BBS: add mutex for protection of gcode result
|
||||
mutable std::mutex result_mutex;
|
||||
|
||||
@@ -33,7 +33,7 @@ class PrintObject;
|
||||
class Print;
|
||||
|
||||
namespace FillAdaptive {
|
||||
struct Octrees;
|
||||
struct RegionOctrees;
|
||||
};
|
||||
|
||||
namespace FillLightning {
|
||||
@@ -207,10 +207,9 @@ public:
|
||||
static bool is_perimeter_compatible(const Print& print, const PrintRegion& a, const PrintRegion& b);
|
||||
void make_perimeters();
|
||||
// Phony version of make_fills() without parameters for Perl integration only.
|
||||
void make_fills() { this->make_fills(nullptr, nullptr); }
|
||||
void make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
|
||||
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees *adaptive_fill_octrees,
|
||||
const FillAdaptive::Octrees *support_fill_octrees,
|
||||
void make_fills() { this->make_fills(nullptr); }
|
||||
void make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
|
||||
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees *fill_octrees,
|
||||
FillLightning::Generator* lightning_generator) const;
|
||||
void make_ironing();
|
||||
// Returns the filament id (1-based) the region is ironed with, or -1 when the
|
||||
|
||||
@@ -1045,7 +1045,28 @@ void do_boolean(McutMesh& srcMesh, const McutMesh& cutMesh, const std::string& b
|
||||
// But we can force it to work by spliting the src mesh into disconnected components,
|
||||
// and do booleans seperately, then merge all the results.
|
||||
indexed_triangle_set all_its;
|
||||
if (boolean_opts == "UNION" || boolean_opts == "A_NOT_B") {
|
||||
if (boolean_opts == "A_NOT_B") {
|
||||
// Each cut can leave the source with several disconnected components, which mcut rejects
|
||||
// in the next dispatch, so re-split after every cut part (e.g. each letter of a text).
|
||||
std::vector<indexed_triangle_set> parts = std::move(src_parts);
|
||||
for (size_t j = 0; j < cut_parts.size(); j++) {
|
||||
auto cut_part = triangle_mesh_to_mcut(cut_parts[j]);
|
||||
std::vector<indexed_triangle_set> next_parts;
|
||||
for (indexed_triangle_set &part : parts) {
|
||||
auto src_part = triangle_mesh_to_mcut(part);
|
||||
if (do_boolean_single(*src_part, *cut_part, boolean_opts)) {
|
||||
TriangleMesh tri_part = mcut_to_triangle_mesh(*src_part);
|
||||
std::vector<indexed_triangle_set> pieces = its_split(tri_part.its);
|
||||
std::move(pieces.begin(), pieces.end(), std::back_inserter(next_parts));
|
||||
} else
|
||||
next_parts.emplace_back(std::move(part));
|
||||
}
|
||||
parts = std::move(next_parts);
|
||||
}
|
||||
for (const indexed_triangle_set &part : parts)
|
||||
its_merge(all_its, part);
|
||||
}
|
||||
else if (boolean_opts == "UNION") {
|
||||
for (size_t i = 0; i < src_parts.size(); i++) {
|
||||
auto src_part = triangle_mesh_to_mcut(src_parts[i]);
|
||||
for (size_t j = 0; j < cut_parts.size(); j++) {
|
||||
|
||||
@@ -583,7 +583,7 @@ private:
|
||||
void discover_horizontal_shells();
|
||||
void combine_infill();
|
||||
void _generate_support_material();
|
||||
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> prepare_adaptive_infill_data(
|
||||
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
|
||||
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
|
||||
FillLightning::GeneratorPtr prepare_lightning_infill_data();
|
||||
|
||||
@@ -616,7 +616,7 @@ private:
|
||||
// so that next call to make_perimeters() performs a union() before computing loops
|
||||
bool m_typed_slices = false;
|
||||
|
||||
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> m_adaptive_fill_octrees;
|
||||
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
|
||||
std::vector<BoundingBox> m_separated_body_bboxes;
|
||||
FillLightning::GeneratorPtr m_lightning_generator;
|
||||
|
||||
|
||||
@@ -848,7 +848,7 @@ void PrintObject::infill()
|
||||
[this](const tbb::blocked_range<size_t>& range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
|
||||
m_print->throw_if_canceled();
|
||||
m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees.first, &m_adaptive_fill_octrees.second, this->m_lightning_generator.get());
|
||||
m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees, this->m_lightning_generator.get());
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1168,14 +1168,27 @@ static std::vector<indexed_triangle_set> split_mesh_by_body(const PrintObject &o
|
||||
return bodies;
|
||||
}
|
||||
|
||||
std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> PrintObject::prepare_adaptive_infill_data(
|
||||
FillAdaptive::RegionOctrees PrintObject::prepare_adaptive_infill_data(
|
||||
const std::vector<std::pair<const Surface *, const Layer *>> &surfaces_w_layer) const
|
||||
{
|
||||
using namespace FillAdaptive;
|
||||
|
||||
auto [adaptive_line_spacing, support_line_spacing] = adaptive_fill_line_spacing(*this);
|
||||
if ((adaptive_line_spacing == 0. && support_line_spacing == 0.) || this->layers().empty())
|
||||
// Orca: Each region fills with the octrees of its own line spacing, shared by the regions of equal spacing.
|
||||
const std::vector<double> line_spacing = adaptive_fill_line_spacing(*this);
|
||||
std::vector<std::pair<double, bool>> spacings; // Line spacing, support cubic.
|
||||
RegionOctrees octrees;
|
||||
octrees.region_set.assign(line_spacing.size(), -1);
|
||||
for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id)
|
||||
if (line_spacing[region_id] > 0.) {
|
||||
const std::pair<double, bool> spacing(line_spacing[region_id], this->printing_region(region_id).config().sparse_infill_pattern == ipSupportCubic);
|
||||
const auto it = std::find(spacings.begin(), spacings.end(), spacing);
|
||||
octrees.region_set[region_id] = int(it - spacings.begin());
|
||||
if (it == spacings.end())
|
||||
spacings.push_back(spacing);
|
||||
}
|
||||
if (spacings.empty() || this->layers().empty())
|
||||
return {};
|
||||
octrees.sets.resize(spacings.size());
|
||||
|
||||
indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set();
|
||||
// Rotate mesh and build octree on it with axis-aligned (standart base) cubes.
|
||||
@@ -1198,44 +1211,50 @@ std::pair<FillAdaptive::Octrees, FillAdaptive::Octrees> PrintObject::prepare_ada
|
||||
}
|
||||
});
|
||||
|
||||
// Orca: Each body gets the octree it has when sliced on its own, from its own triangles.
|
||||
std::pair<Octrees, Octrees> octrees;
|
||||
const size_t num_bodies = m_separated_body_bboxes.size();
|
||||
bool need_object = num_bodies <= 1;
|
||||
// Orca: Each body gets the octree it has when sliced on its own, from its own triangles, for each line spacing
|
||||
// its regions fill with. Body num_bodies stands for the whole object, which serves an object of a single body
|
||||
// and the surfaces of bodies that have no octree of their own.
|
||||
const size_t num_bodies = m_separated_body_bboxes.size();
|
||||
std::vector<std::pair<size_t, size_t>> to_build; // Set, body.
|
||||
std::vector<indexed_triangle_set> body_meshes;
|
||||
std::vector<std::vector<Vec3d>> body_overhangs(num_bodies);
|
||||
if (num_bodies > 1) {
|
||||
const std::vector<indexed_triangle_set> body_meshes = split_mesh_by_body(*this, mesh, num_bodies);
|
||||
need_object = std::any_of(body_meshes.begin(), body_meshes.end(), [](const indexed_triangle_set &its) { return its.indices.empty(); });
|
||||
std::vector<std::vector<Vec3d>> body_overhangs(num_bodies);
|
||||
body_meshes = split_mesh_by_body(*this, mesh, num_bodies);
|
||||
for (size_t i = 0; i < surfaces_w_layer.size(); ++ i)
|
||||
if (const int body = separated_body_at(*surfaces_w_layer[i].second, surfaces_w_layer[i].first->expolygon.contour.points.front()); body >= 0)
|
||||
append(body_overhangs[body], overhangs[i]);
|
||||
if (adaptive_line_spacing)
|
||||
octrees.first.bodies.resize(num_bodies);
|
||||
if (support_line_spacing)
|
||||
octrees.second.bodies.resize(num_bodies);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_bodies), [&, adaptive_spacing = adaptive_line_spacing, support_spacing = support_line_spacing](
|
||||
const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t body = range.begin(); body < range.end(); ++ body) {
|
||||
m_print->throw_if_canceled();
|
||||
if (body_meshes[body].indices.empty())
|
||||
continue;
|
||||
if (adaptive_spacing)
|
||||
octrees.first.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], adaptive_spacing, false);
|
||||
if (support_spacing)
|
||||
octrees.second.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], support_spacing, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
std::vector<std::vector<char>> fills(spacings.size(), std::vector<char>(num_bodies + 1, false));
|
||||
for (const Layer *layer : m_layers)
|
||||
for (size_t region_id = 0; region_id < layer->regions().size() && region_id < octrees.region_set.size(); ++ region_id)
|
||||
if (const int set = octrees.region_set[region_id]; set >= 0)
|
||||
for (const Surface &surface : layer->regions()[region_id]->fill_surfaces) {
|
||||
const int body = separated_body_at(*layer, surface.expolygon.contour.points.front());
|
||||
fills[set][body >= 0 && ! body_meshes[body].indices.empty() ? size_t(body) : num_bodies] = true;
|
||||
}
|
||||
for (size_t set = 0; set < spacings.size(); ++ set) {
|
||||
octrees.sets[set].bodies.resize(num_bodies);
|
||||
for (size_t body = 0; body <= num_bodies; ++ body)
|
||||
if (fills[set][body])
|
||||
to_build.emplace_back(set, body);
|
||||
}
|
||||
} else
|
||||
for (size_t set = 0; set < spacings.size(); ++ set)
|
||||
to_build.emplace_back(set, num_bodies);
|
||||
|
||||
// and gather them.
|
||||
for (size_t i = 1; i < overhangs.size(); ++ i)
|
||||
append(overhangs.front(), std::move(overhangs[i]));
|
||||
|
||||
// Orca: The object's octree only serves bodies that have none of their own.
|
||||
if (need_object && adaptive_line_spacing)
|
||||
octrees.first.object = build_octree(mesh, overhangs.front(), adaptive_line_spacing, false);
|
||||
if (need_object && support_line_spacing)
|
||||
octrees.second.object = build_octree(mesh, overhangs.front(), support_line_spacing, true);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, to_build.size()), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t i = range.begin(); i < range.end(); ++ i) {
|
||||
m_print->throw_if_canceled();
|
||||
const auto [set, body] = to_build[i];
|
||||
const bool object = body == num_bodies;
|
||||
(object ? octrees.sets[set].object : octrees.sets[set].bodies[body]) =
|
||||
build_octree(object ? mesh : body_meshes[body], object ? overhangs.front() : body_overhangs[body], spacings[set].first,
|
||||
spacings[set].second);
|
||||
}
|
||||
});
|
||||
return octrees;
|
||||
}
|
||||
|
||||
@@ -3077,8 +3096,7 @@ void PrintObject::bridge_over_infill()
|
||||
for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) {
|
||||
size_t lidx = layers_to_generate_infill[job_idx];
|
||||
infill_lines.at(
|
||||
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees.first,
|
||||
&po->m_adaptive_fill_octrees.second,
|
||||
lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees,
|
||||
po->m_lightning_generator.get());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1248,52 +1248,81 @@ bool triangles_overlap(const Tri2 &a, const Tri2 &b, float eps)
|
||||
struct NetGrid
|
||||
{
|
||||
static constexpr int BIG_SPAN = 16;
|
||||
float cell;
|
||||
float eps;
|
||||
std::unordered_map<uint64_t, std::vector<Tri2>> cells;
|
||||
std::vector<Tri2> big;
|
||||
// Each stored triangle keeps its own bounding box. Overlap testing is dominated by rejects - a cell
|
||||
// holds every triangle whose box touches it, and a candidate meets only a couple of them for real -
|
||||
// so paying six floats per entry to answer most of those rejects with four comparisons, instead of a
|
||||
// full triangle intersection, is what makes the net affordable. Measured on a 42k-triangle patch the
|
||||
// grid ran ~19 million candidate pairs per net, nearly all of them misses, and rejecting them this
|
||||
// way took the net from ~175 ms to ~53 ms.
|
||||
//
|
||||
// The box rides inside the entry rather than in a parallel array: splitting them to scan boxes back
|
||||
// to back was tried and came out slower, because each bucket then grows two vectors instead of one.
|
||||
struct Entry
|
||||
{
|
||||
Tri2 tri;
|
||||
Vec2f lo, hi;
|
||||
};
|
||||
float cell;
|
||||
float eps;
|
||||
std::unordered_map<uint64_t, std::vector<Entry>> cells;
|
||||
std::vector<Entry> big;
|
||||
|
||||
static uint64_t key(int x, int y) { return (uint64_t(uint32_t(x)) << 32) | uint32_t(y); }
|
||||
bool range(const Tri2 &t, int &x0, int &y0, int &x1, int &y1) const
|
||||
static Entry entry(const Tri2 &t)
|
||||
{
|
||||
return Entry{ t, t[0].cwiseMin(t[1]).cwiseMin(t[2]), t[0].cwiseMax(t[1]).cwiseMax(t[2]) };
|
||||
}
|
||||
bool range(const Vec2f &lo, const Vec2f &hi, int &x0, int &y0, int &x1, int &y1) const
|
||||
{
|
||||
const Vec2f lo = t[0].cwiseMin(t[1]).cwiseMin(t[2]), hi = t[0].cwiseMax(t[1]).cwiseMax(t[2]);
|
||||
x0 = int(std::floor(lo.x() / cell));
|
||||
y0 = int(std::floor(lo.y() / cell));
|
||||
x1 = int(std::floor(hi.x() / cell));
|
||||
y1 = int(std::floor(hi.y() / cell));
|
||||
return x1 - x0 <= BIG_SPAN && y1 - y0 <= BIG_SPAN;
|
||||
}
|
||||
// Boxes grown by eps on both sides, to match the tolerance triangles_overlap() itself works to: a
|
||||
// reject here must never discard a pair that test would have called touching.
|
||||
bool boxes_apart(const Entry &a, const Entry &b) const
|
||||
{
|
||||
return a.hi.x() + eps < b.lo.x() || b.hi.x() + eps < a.lo.x() || a.hi.y() + eps < b.lo.y() ||
|
||||
b.hi.y() + eps < a.lo.y();
|
||||
}
|
||||
bool hits(const Entry &q, const std::vector<Entry> &bucket) const
|
||||
{
|
||||
for (const Entry &b : bucket)
|
||||
if (!boxes_apart(q, b) && triangles_overlap(q.tri, b.tri, eps))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
bool overlaps(const Tri2 &t) const
|
||||
{
|
||||
for (const Tri2 &b : big)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
const Entry q = entry(t);
|
||||
if (hits(q, big))
|
||||
return true;
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(t, x0, y0, x1, y1)) {
|
||||
for (const auto &[k, tris] : cells)
|
||||
for (const Tri2 &b : tris)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
if (!range(q.lo, q.hi, x0, y0, x1, y1)) {
|
||||
for (const auto &[k, bucket] : cells)
|
||||
if (hits(q, bucket))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
if (const auto it = cells.find(key(x, y)); it != cells.end())
|
||||
for (const Tri2 &b : it->second)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
if (const auto it = cells.find(key(x, y)); it != cells.end() && hits(q, it->second))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
void insert(const Tri2 &t)
|
||||
{
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(t, x0, y0, x1, y1)) {
|
||||
big.push_back(t);
|
||||
const Entry e = entry(t);
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(e.lo, e.hi, x0, y0, x1, y1)) {
|
||||
big.push_back(e);
|
||||
return;
|
||||
}
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
cells[key(x, y)].push_back(t);
|
||||
cells[key(x, y)].push_back(e);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
@@ -1305,11 +1334,20 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
if (n <= 1)
|
||||
return islands;
|
||||
|
||||
// Chart adjacency, with one representative shared edge per adjacent pair.
|
||||
// Chart adjacency, with one representative shared edge per adjacent pair: the fold line the pair is
|
||||
// unfolded about.
|
||||
//
|
||||
// Which edge that is matters, because two charts can touch along more than one run. A chart cut open
|
||||
// to flatten it - a ring opened by segment_into_charts(), say - touches its other half along *both*
|
||||
// sides of the cut. Folding is rigid, so only the run the fold line belongs to comes out matching;
|
||||
// every other run is left mismatched, and a mismatched run is exactly where the texture visibly
|
||||
// jumps. Taking whichever edge the map happened to yield first therefore left the long side broken
|
||||
// about as often as the short one. The fold line is picked from the longest run instead, so what is
|
||||
// left discontinuous is the shortest boundary the pair has.
|
||||
const auto edges = build_shared_edges(unwrap);
|
||||
struct PairEdge { ChartEdge a, b; };
|
||||
std::map<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
||||
struct SharedEdge { PairEdge fold; int base_lo = -1, base_hi = -1; float length = 0.f; };
|
||||
std::map<std::pair<int, int>, std::vector<SharedEdge>> pair_shared;
|
||||
for (const auto &[base_edge, list] : edges) {
|
||||
for (size_t i = 0; i < list.size(); ++i)
|
||||
for (size_t j = i + 1; j < list.size(); ++j) {
|
||||
@@ -1317,14 +1355,50 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n)
|
||||
continue;
|
||||
const std::pair<int, int> pk{ std::min(c1, c2), std::max(c1, c2) };
|
||||
if (pair_edge.count(pk))
|
||||
continue; // keep the first shared edge as the fold line for this pair
|
||||
pair_edge[pk] = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
|
||||
adj[size_t(pk.first)].push_back(pk.second);
|
||||
adj[size_t(pk.second)].push_back(pk.first);
|
||||
SharedEdge se;
|
||||
se.fold = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
|
||||
se.base_lo = base_edge.first;
|
||||
se.base_hi = base_edge.second;
|
||||
// The unwrap is scaled to true surface area, so a uv distance is a length in mm.
|
||||
se.length = (unwrap.uvs[size_t(se.fold.a.uv_lo)] - unwrap.uvs[size_t(se.fold.a.uv_hi)]).norm();
|
||||
pair_shared[pk].push_back(se);
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::map<std::pair<int, int>, float> pair_weight; // length of the run each pair folds across
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
||||
for (const auto &[pk, shared] : pair_shared) {
|
||||
// Group the pair's shared edges into runs - edges joined end to end through a base vertex - and
|
||||
// total each run's length.
|
||||
std::unordered_map<int, int> local;
|
||||
for (const SharedEdge &se : shared)
|
||||
for (const int v : { se.base_lo, se.base_hi })
|
||||
local.emplace(v, int(local.size()));
|
||||
UnionFind runs(local.size());
|
||||
for (const SharedEdge &se : shared)
|
||||
runs.unite(local[se.base_lo], local[se.base_hi]);
|
||||
|
||||
std::unordered_map<int, float> run_length;
|
||||
std::unordered_map<int, size_t> run_first;
|
||||
for (size_t i = 0; i < shared.size(); ++i) {
|
||||
const int root = runs.find(local[shared[i].base_lo]);
|
||||
run_length[root] += shared[i].length;
|
||||
run_first.emplace(root, i);
|
||||
}
|
||||
int best_root = -1;
|
||||
float best_len = -1.f;
|
||||
for (const auto &[root, len] : run_length)
|
||||
if (len > best_len) { best_len = len; best_root = root; }
|
||||
if (best_root < 0)
|
||||
continue;
|
||||
|
||||
pair_edge[pk] = shared[run_first[best_root]].fold;
|
||||
pair_weight[pk] = best_len;
|
||||
adj[size_t(pk.first)].push_back(pk.second);
|
||||
adj[size_t(pk.second)].push_back(pk.first);
|
||||
}
|
||||
|
||||
// Per chart: its vertices, its triangles and its flattened area.
|
||||
std::vector<std::vector<int>> chart_verts(static_cast<size_t>(n)), chart_tris(static_cast<size_t>(n));
|
||||
std::vector<float> chart_area(static_cast<size_t>(n), 0.f);
|
||||
@@ -1378,15 +1452,29 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
for (const int t : chart_tris[size_t(root)])
|
||||
grid.insert(placed(m, t));
|
||||
}
|
||||
std::queue<int> q;
|
||||
q.push(root);
|
||||
// Grown strongest-adjacency-first (Prim, not breadth-first): a chart is folded onto whichever
|
||||
// neighbour it shares the longest boundary with, among everything reachable so far. Order matters
|
||||
// because only the fold a chart is actually reached by comes out matching - every other boundary
|
||||
// it has is left to chance. Taking neighbours in breadth-first order, biggest-area first, let a
|
||||
// far-off branch claim a chart across a short boundary before its true neighbour was reached, and
|
||||
// the long boundary they shared then stayed broken. That is the visible seam next to a hole: a
|
||||
// ring is cut into two halves that share a long boundary, and whichever half was reached first
|
||||
// took the other one along some unrelated edge.
|
||||
using Candidate = std::pair<float, std::pair<int, int>>; // weight, (from, to)
|
||||
std::priority_queue<Candidate> q;
|
||||
const auto push_neighbours = [&](int p) {
|
||||
for (const int c : adj[size_t(p)])
|
||||
if (net_of[size_t(c)] < 0 && !chart_tris[size_t(c)].empty()) {
|
||||
const auto w = pair_weight.find({ std::min(p, c), std::max(p, c) });
|
||||
q.push({ w == pair_weight.end() ? 0.f : w->second, { p, c } });
|
||||
}
|
||||
};
|
||||
push_neighbours(root);
|
||||
while (!q.empty()) {
|
||||
const int p = q.front();
|
||||
const auto [weight, link] = q.top();
|
||||
q.pop();
|
||||
std::vector<int> neighbours = adj[size_t(p)];
|
||||
std::stable_sort(neighbours.begin(), neighbours.end(),
|
||||
[&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; });
|
||||
for (const int c : neighbours) {
|
||||
const int p = link.first, c = link.second;
|
||||
{
|
||||
if (net_of[size_t(c)] >= 0 || chart_tris[size_t(c)].empty())
|
||||
continue;
|
||||
const auto it = pair_edge.find({ std::min(p, c), std::max(p, c) });
|
||||
@@ -1417,7 +1505,7 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
for (const Tri2 &t : tris)
|
||||
grid.insert(t);
|
||||
net_of[size_t(c)] = net;
|
||||
q.push(c);
|
||||
push_neighbours(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5053,4 +5141,5 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include "slic3r/GUI/Widgets/HyperLink.hpp"
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
@@ -48,6 +47,7 @@
|
||||
#include <wx/panel.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/valtext.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/peninfobase.h>
|
||||
@@ -103,6 +103,14 @@ void AMSMaterialsSetting::create()
|
||||
|
||||
m_sizer_button->Add(0, 0, 1, wxEXPAND, 0);
|
||||
|
||||
// Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic).
|
||||
m_wiki_ctrl = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
|
||||
m_wiki_ctrl->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
m_wiki_ctrl->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
m_wiki_ctrl->SetCanFocus(false);
|
||||
m_wiki_ctrl->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
m_button_confirm = new Button(this, _L("Confirm"));
|
||||
m_button_confirm->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
|
||||
m_button_confirm->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_ok, this);
|
||||
@@ -115,8 +123,9 @@ void AMSMaterialsSetting::create()
|
||||
m_button_close->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
|
||||
m_button_close->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_close, this);
|
||||
|
||||
m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
|
||||
m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
|
||||
m_sizer_button->Add(m_wiki_ctrl, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
|
||||
m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10));
|
||||
m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10));
|
||||
m_sizer_button->Add(m_button_close, 0, wxALIGN_CENTER, 0);
|
||||
|
||||
m_sizer_main->Add(m_panel_normal, 0, wxALL, FromDIP(2));
|
||||
@@ -340,11 +349,7 @@ void AMSMaterialsSetting::create_panel_kn(wxWindow* parent)
|
||||
m_ratio_text->SetForegroundColour(wxColour(50, 58, 61));
|
||||
m_ratio_text->SetFont(Label::Head_14);
|
||||
|
||||
// Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic).
|
||||
wxString link_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
|
||||
m_wiki_ctrl = new HyperLink(parent, _L("Wiki Guide"), link_url);
|
||||
cali_title_sizer->Add(m_ratio_text, 0, wxALIGN_CENTER_VERTICAL);
|
||||
cali_title_sizer->Add(m_wiki_ctrl, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
wxBoxSizer *m_sizer_cali_resutl = new wxBoxSizer(wxHORIZONTAL);
|
||||
// pa profile
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include "slic3r/Utils/CalibUtils.hpp"
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/colour.h>
|
||||
@@ -194,7 +193,7 @@ protected:
|
||||
|
||||
wxPanel * m_panel_kn;
|
||||
wxStaticText* m_ratio_text;
|
||||
HyperLink * m_wiki_ctrl;
|
||||
Button * m_wiki_ctrl;
|
||||
wxStaticText* m_k_param;
|
||||
TextInput* m_input_k_val;
|
||||
wxStaticText* m_n_param;
|
||||
|
||||
@@ -287,7 +287,13 @@ void BackgroundSlicingProcess::process_fff()
|
||||
// GCodeProcessorResult, so m_gcode_result->nozzle_group_result (consumed by the H2C print-dispatch
|
||||
// nozzle mapping) survives post-processing. No preservation guard is needed on this path.
|
||||
if (m_fff_print->is_BBL_printer()) {
|
||||
run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config());
|
||||
if (run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config()))
|
||||
// The scripts/plugins rewrote the very file the G-code viewer memory-maps, so every byte
|
||||
// offset in m_gcode_result->lines_ends (built while exporting the pre-processed G-code) is
|
||||
// now stale and GCodeWindow would slice the file mid-line. Re-scan it. Note this only
|
||||
// realigns the line framing: if a script inserts or removes lines, the moves' gcode_id
|
||||
// still refers to the pre-processed numbering and the highlighted line stays shifted.
|
||||
m_gcode_result->rebuild_lines_ends();
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": export gcode finished");
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include "slic3r/GUI/PresetComboBoxes.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include <wx/anybutton.h>
|
||||
#include "slic3r/GUI/Widgets/HyperLink.hpp"
|
||||
#include <wx/utils.h>
|
||||
#include <wx/arrstr.h>
|
||||
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
|
||||
@@ -494,7 +493,10 @@ void CaliPageCaption::init_bitmaps() {
|
||||
void CaliPageCaption::create_wiki(wxWindow* parent)
|
||||
{
|
||||
// ORCA standardized HyperLink
|
||||
m_wiki_text = new HyperLink(parent, _L("Wiki Guide"));
|
||||
m_wiki_text = new Button(parent, "", "toolbar_wiki", 0, 15);
|
||||
m_wiki_text->SetToolTip(_L("Wiki Guide"));
|
||||
m_wiki_text->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
m_wiki_text->SetCanFocus(false);
|
||||
m_wiki_text->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
|
||||
if (!m_wiki_url.empty())
|
||||
wxLaunchDefaultBrowser(m_wiki_url);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/AMSControl.hpp"
|
||||
#include "Widgets/ProgressBar.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "PresetComboBoxes.hpp"
|
||||
|
||||
@@ -155,7 +154,7 @@ private:
|
||||
void init_bitmaps();
|
||||
void create_wiki(wxWindow* parent);
|
||||
|
||||
HyperLink* m_wiki_text; // ORCA
|
||||
Button* m_wiki_text; // ORCA
|
||||
wxString m_wiki_url;
|
||||
ScalableBitmap m_prev_bmp_normal;
|
||||
ScalableBitmap m_prev_bmp_hover;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "wx/dcgraph.h"
|
||||
#include "I18N.hpp"
|
||||
#include "PartPlate.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include <wx/colour.h>
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
@@ -27,6 +26,7 @@
|
||||
#include "slic3r/GUI/Widgets/StaticBox.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/Widgets/SwitchButton.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include <wx/tglbtn.h>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
@@ -203,18 +203,24 @@ FilamentGroupPopup::FilamentGroupPopup(wxWindow *parent) : PopupWindow(parent, w
|
||||
{
|
||||
wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
// ORCA Unified hyperlinks
|
||||
video_link = new HyperLink(this, _L("Video tutorial"));
|
||||
video_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent& e)
|
||||
{
|
||||
play_dual_extruder_slice_video();
|
||||
wxGetApp().app_config->set("play_slicing_video", "false");
|
||||
});
|
||||
button_sizer->Add(video_link, 0, wxLEFT, horizontal_margin + FromDIP(3));
|
||||
button_sizer->AddStretchSpacer();
|
||||
|
||||
wiki_link = new HyperLink(this, _L("Wiki Guide"));
|
||||
wiki_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { open_filament_group_wiki(); });
|
||||
button_sizer->Add(wiki_link, 0, wxLEFT, horizontal_margin);
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + "https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {open_filament_group_wiki();}));
|
||||
|
||||
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
|
||||
video_btn->SetToolTip(_L("Video Guide"));
|
||||
video_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
|
||||
video_btn->SetCanFocus(false);
|
||||
video_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {
|
||||
play_dual_extruder_slice_video();
|
||||
wxGetApp().app_config->set("play_slicing_video", "false");
|
||||
}));
|
||||
|
||||
button_sizer->Add(wiki_btn , 0, wxLEFT, horizontal_margin + FromDIP(3));
|
||||
button_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
|
||||
|
||||
top_sizer->Add(button_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, horizontal_margin);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
FilamentGroupPopup(wxWindow *parent);
|
||||
void tryPopup(Plater* plater,PartPlate* plate, bool slice_all);
|
||||
void tryClose();
|
||||
void Dismiss() override;
|
||||
|
||||
FilamentMapMode GetSelectedMode() const { return m_mode; }
|
||||
private:
|
||||
@@ -40,7 +41,6 @@ private:
|
||||
void OnLeaveWindow(wxMouseEvent &);
|
||||
void OnEnterWindow(wxMouseEvent &);
|
||||
void OnTimer(wxTimerEvent &event);
|
||||
void Dismiss();
|
||||
|
||||
void CreateBmps();
|
||||
|
||||
@@ -80,10 +80,6 @@ private:
|
||||
wxBitmap unchecked_hover_bmp;
|
||||
wxBitmap global_tag_bmp;
|
||||
|
||||
|
||||
wxStaticText *wiki_link;
|
||||
wxStaticText *video_link;
|
||||
|
||||
StaticBox *m_smart_filament_panel{nullptr};
|
||||
wxSizerItem *m_smart_filament_spacer{nullptr};
|
||||
SwitchButton *m_smart_filament_switch{nullptr};
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/GLShader.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <math.h>
|
||||
#include "libvgcode/include/Viewer.hpp"
|
||||
@@ -889,10 +890,16 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, f
|
||||
auto update_lines = [this](uint64_t start_id, uint64_t end_id) {
|
||||
std::vector<Line> ret;
|
||||
ret.reserve(end_id - start_id + 1);
|
||||
// Orca: m_lines_ends indexes into a memory mapping, so it must be clamped to the mapping. If the
|
||||
// file was modified behind our back (an in-place post-processing script that shrank it), an
|
||||
// unchecked read is an access violation, which the caller's try/catch cannot catch on Windows.
|
||||
const size_t file_size = m_file.size();
|
||||
for (uint64_t id = start_id; id <= end_id; ++id) {
|
||||
// read line from file
|
||||
const size_t start = id == 1 ? 0 : m_lines_ends[id - 2];
|
||||
const size_t original_len = m_lines_ends[id - 1] - start;
|
||||
// Keep one entry per id: render() indexes m_lines by (id - start_id).
|
||||
const size_t start = id == 1 ? 0 : std::min(m_lines_ends[id - 2], file_size);
|
||||
const size_t end = std::min(m_lines_ends[id - 1], file_size);
|
||||
const size_t original_len = end > start ? end - start : 0;
|
||||
// A character is four bytes at most, so 55 of them always fit in 220.
|
||||
const size_t len = std::min(original_len, (size_t) 55 * 4);
|
||||
std::string gline(m_file.data() + start, len);
|
||||
@@ -3127,27 +3134,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
}
|
||||
};
|
||||
|
||||
auto link_filament_group_wiki = [&](const std::string& label) {
|
||||
ImVec2 wiki_part_size = ImGui::CalcTextSize(label.c_str());
|
||||
ImColor HyperColor = ImColor(0, 150, 136, 255); // ORCA match color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, HyperColor.Value);
|
||||
imgui.text(label.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// ORCA use underline to match hyperlink style
|
||||
ImVec2 lineEnd = ImGui::GetItemRectMax();
|
||||
lineEnd.y -= 2.0f;
|
||||
ImVec2 lineStart = lineEnd;
|
||||
lineStart.x = ImGui::GetItemRectMin().x;
|
||||
ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, HyperColor);
|
||||
// click behavior
|
||||
if (ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), true)) {
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
open_filament_group_wiki();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto draw_dash_line = [&](ImDrawList* draw_list, int dash_length = 5, int gap_length = 3) {
|
||||
ImVec2 p1 = ImGui::GetCursorScreenPos();
|
||||
ImVec2 p2 = ImVec2(p1.x + ImGui::GetContentRegionAvail().x, p1.y);
|
||||
@@ -3221,7 +3207,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
else
|
||||
tips_count = 5;
|
||||
|
||||
float AMS_container_height = ams_item_height + line_height * tips_count + line_height;
|
||||
float AMS_container_height = ams_item_height + line_height * tips_count + line_height + window_padding * 2.f;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // this shold be 0 since its child of gcodeviewer
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(window_padding * 3, 0));
|
||||
@@ -3236,10 +3222,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(1.0f,1.0f,1.0f,0.6f));
|
||||
imgui.bold_text(_u8L("Filament Grouping"));
|
||||
ImGui::SameLine();
|
||||
std::string tip_str = _u8L("Why this grouping");
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(tip_str.c_str()).x);
|
||||
link_filament_group_wiki(tip_str);
|
||||
ImGui::Separator();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
@@ -3344,6 +3326,9 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
}
|
||||
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
GLGizmoUtils::render_wiki_guide_button(*wxGetApp().plater()->get_current_canvas3D(), m_scale,"https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
|
||||
ImGui::SameLine();
|
||||
|
||||
if (!is_optimal_group) {
|
||||
link_text_set_to_optional(_u8L("Set to Optimal"));
|
||||
ImGui::SameLine();
|
||||
@@ -3352,10 +3337,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
}
|
||||
link_text(_u8L("Regroup filament"));
|
||||
|
||||
ImGui::SameLine();
|
||||
std::string wiki_str = _u8L("Wiki Guide"); // ORCA
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(wiki_str.c_str()).x);
|
||||
link_filament_group_wiki(wiki_str);
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "slic3r/GUI/MsgDialog.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/TextureLibrary.hpp"
|
||||
#include "slic3r/GUI/TextureProjectorFrame.hpp"
|
||||
#include "slic3r/GUI/UVEditorCanvas.hpp"
|
||||
@@ -407,11 +408,48 @@ GLGizmoTextureDisplacement::GLGizmoTextureDisplacement(GLCanvas3D& parent, const
|
||||
|
||||
bool GLGizmoTextureDisplacement::on_init()
|
||||
{
|
||||
m_shortcut = Shortcut::GizmoDisplacement;
|
||||
const wxString ctrl = GUI::shortkey_ctrl_prefix();
|
||||
const wxString alt = GUI::shortkey_alt_prefix();
|
||||
const wxString shift = GUI::shortkey_shift_prefix();
|
||||
|
||||
|
||||
m_desc["cursor_size"] = _L("Brush size");
|
||||
m_desc["circle"] = _L("Circle");
|
||||
m_desc["sphere"] = _L("Sphere");
|
||||
m_desc["remove_layer"] = _L("Remove");
|
||||
m_desc["bake"] = _L_CONTEXT("Bake", "Texture Displacement");
|
||||
|
||||
|
||||
m_desc["paint"] = _L("Paint");
|
||||
m_desc["erase"] = _L("Erase");
|
||||
m_desc["gap_area"] = _L("Gap area");
|
||||
m_desc["smart_fill_angle"] = _L("Smart fill angle");
|
||||
m_desc["toggle_wireframe"] = _L("Toggle Wireframe");
|
||||
|
||||
std::pair<wxString, wxString> paint_shortcut = {_L("Left mouse button"), m_desc["paint"]};
|
||||
std::pair<wxString, wxString> erase_shortcut = {shift + _L("Left mouse button"), m_desc["erase"]};
|
||||
std::pair<wxString, wxString> toggle_wireframe_shortcut = {alt + shift + _L_CONTEXT("Enter", "Keyboard Shortcut"), m_desc["toggle_wireframe"]};
|
||||
|
||||
m_shortcuts_brush = {
|
||||
paint_shortcut,
|
||||
erase_shortcut,
|
||||
{ctrl + _L("Mouse wheel"), m_desc["cursor_size"]},
|
||||
toggle_wireframe_shortcut
|
||||
};
|
||||
|
||||
m_shortcuts_bucket_fill = {
|
||||
paint_shortcut,
|
||||
erase_shortcut,
|
||||
{ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]},
|
||||
toggle_wireframe_shortcut
|
||||
};
|
||||
|
||||
m_shortcuts_gap_fill = {
|
||||
{ctrl + _L("Mouse wheel"), m_desc["gap_area"]},
|
||||
toggle_wireframe_shortcut
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -523,7 +561,15 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
// volume for a shaded pass that then draws nothing is what made the model vanish - most obviously
|
||||
// with zero layers, but equally with a layer that has no texture picked yet.
|
||||
const bool use_shaded = m_use_shaded_preview && m_shaded_preview_glmodel.is_initialized() && shaded_preview_ready();
|
||||
const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized();
|
||||
// Checker/Distortion are built from the *base* patch and drawn with a polygon offset, which biases
|
||||
// depth values - it does not move the geometry. It therefore cannot win against a surface that
|
||||
// genuinely stands in front, and the displaced preview does exactly that: it rises above the base
|
||||
// surface by the layer's depth. Drawn underneath a UV-check overlay it simply occludes it, which is
|
||||
// why those two modes looked like they did nothing. Leave it out and let the undisplaced volume show
|
||||
// through instead (toggle_model_objects_visibility below) - that one *is* coincident with the
|
||||
// overlay, which is what the offset assumes, and it is the surface whose mapping is being inspected.
|
||||
const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None &&
|
||||
m_preview_glmodel.is_initialized();
|
||||
// In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is
|
||||
// looking at, so the opaque paint-selection highlight must not be drawn on top of it - same
|
||||
// reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area
|
||||
@@ -551,7 +597,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
render_triangles(selection);
|
||||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||||
}
|
||||
} else if (show_paint_overlay) {
|
||||
} else {
|
||||
// render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the
|
||||
// selector's colours), not an overlay on top of one - so it still has to run under a UV-check
|
||||
// overlay, or nothing draws the surface at all and the checker floats alone over an empty scene.
|
||||
// Deliberately without the depth bias the branch above applies: the checker/heatmap is drawn later
|
||||
// with its own -1 offset and has to win against this. Biasing both by the same amount is what made
|
||||
// the painted area cover the checker and is why this call used to be skipped outright.
|
||||
render_triangles(selection);
|
||||
}
|
||||
|
||||
@@ -5313,6 +5365,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
return;
|
||||
ModelVolume *mv = texture_volume();
|
||||
|
||||
float scale = m_parent.get_scale();
|
||||
#ifdef WIN32
|
||||
int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
|
||||
scale *= (float) dpi / (float) DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
const float approx_height = m_imgui->scaled(24.f);
|
||||
y = std::min(y, bottom_limit - approx_height);
|
||||
|
||||
@@ -5742,14 +5800,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const int cur_mode = m_use_shaded_preview ? 1 :
|
||||
m_uv_check_mode == UVCheckMode::Checker ? 2 :
|
||||
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
|
||||
int new_mode = cur_mode;
|
||||
bool wf_toggle = false;
|
||||
const wxString distortion_na = active == nullptr ? _L("Add a layer first.") :
|
||||
active->projection_method != TextureProjectionMethod::LSCM ?
|
||||
_L("Needs the active layer mapped with Unwrap (LSCM).") :
|
||||
wxString();
|
||||
// Distortion over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
|
||||
if (cur_mode == 3 && !distortion_na.empty())
|
||||
int new_mode = cur_mode;
|
||||
bool wf_toggle = false;
|
||||
bool open_uv_editor = false;
|
||||
// Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second
|
||||
// its stretch - so they only mean anything for a layer mapped with Unwrap (LSCM). On the default
|
||||
// triplanar mapping (or cylindrical / spherical / from view) they are faded out with the reason in the
|
||||
// tooltip, rather than being offered and then showing nothing.
|
||||
const wxString uv_view_na = active == nullptr ? _L("Add a layer first.") :
|
||||
active->projection_method != TextureProjectionMethod::LSCM ?
|
||||
_L("Only for a layer mapped with Unwrap (LSCM) - set the "
|
||||
"active layer's Mapping to Unwrap to use this view.") :
|
||||
wxString();
|
||||
// Either view over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
|
||||
if ((cur_mode == 2 || cur_mode == 3) && !uv_view_na.empty())
|
||||
new_mode = 0;
|
||||
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
@@ -5768,12 +5832,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
|
||||
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
|
||||
"texture is undistorted; where they stretch, it will too")))
|
||||
new_mode = 2;
|
||||
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
new_mode = 2;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"),
|
||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap"), distortion_na))
|
||||
new_mode = 3;
|
||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
new_mode = 3;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
vsep(icon_md);
|
||||
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"),
|
||||
_L("Wireframe - overlay the mesh edges; independent of the view above")))
|
||||
@@ -5788,6 +5860,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(_u8L("Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting "
|
||||
"or dragging a slider starts to stutter - the preview then waits until you let go."));
|
||||
|
||||
// Both are views of the unwrap, so picking one brings the UV editor up with it - including when that
|
||||
// view is already the active one and only the pane is missing.
|
||||
if (open_uv_editor && !m_show_uv_editor) {
|
||||
m_show_uv_editor = true;
|
||||
if (new_mode == cur_mode)
|
||||
update_uv_editor(); // otherwise apply_view_mode() below does it
|
||||
}
|
||||
if (new_mode != cur_mode)
|
||||
apply_view_mode(new_mode);
|
||||
if (wf_toggle) {
|
||||
@@ -6891,18 +6970,34 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
|
||||
const float button_h = std::round(frame_h * 1.25f);
|
||||
const float third = std::floor((panel_w - style.ItemSpacing.x) / 3.f);
|
||||
if (busy) {
|
||||
if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(third, button_h)))
|
||||
wxGetApp().plater()->get_ui_job_worker().cancel_all();
|
||||
hover_tip(_u8L("Stops the bake. Whatever it had already finished stays on the model, and can be undone."));
|
||||
} else {
|
||||
if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(third, button_h)))
|
||||
m_parent.reset_all_gizmos();
|
||||
hover_tip(_u8L("Closes the tool without baking. Your paint, layers and settings stay with the model."));
|
||||
}
|
||||
const float button_w = std::max({
|
||||
ImGui::CalcTextSize(_u8L("Close").c_str()).x,
|
||||
ImGui::CalcTextSize(_u8L("Stop").c_str()).x,
|
||||
ImGui::CalcTextSize(_u8L("Preparing...").c_str()).x,
|
||||
ImGui::CalcTextSize(_u8L("Baking...").c_str()).x,
|
||||
}) + m_imgui->scaled(0.5f);
|
||||
const float row_y = ImGui::GetCursorPosY();
|
||||
const float icon_h = 21.f * scale;
|
||||
const float row_h = std::max(icon_h, button_h);
|
||||
|
||||
const std::vector<std::pair<wxString, wxString>> shortcut =
|
||||
m_tool_type == ToolType::BUCKET_FILL ? m_shortcuts_bucket_fill
|
||||
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
|
||||
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
|
||||
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
|
||||
: std::vector<std::pair<wxString, wxString>>{};
|
||||
|
||||
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
|
||||
GLGizmoUtils::render_tooltip_button(m_imgui, m_parent, shortcut, x, y);
|
||||
ImGui::SameLine();
|
||||
GLGizmoUtils::render_wiki_guide_button(m_parent, scale, "https://www.orcaslicer.com/wiki/print_prepare/prepare_texture_displacement");
|
||||
ImGui::SameLine();
|
||||
GLGizmoUtils::render_video_guide_button(m_parent, scale, "https://www.youtube.com/watch?v=D7w3tG1kdvE");
|
||||
ImGui::PopStyleVar(1);
|
||||
|
||||
ImGui::SameLine(x0 + panel_w - button_w * 2 - style.ItemSpacing.x);
|
||||
|
||||
const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted();
|
||||
const std::string bake_label = m_prepare_in_progress ? _u8L("Preparing...") :
|
||||
m_bake_in_progress ? _u8L("Baking...") :
|
||||
@@ -6910,7 +7005,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
GLGizmoUtils::push_orca_button_style();
|
||||
m_imgui->push_bold_font();
|
||||
m_imgui->disabled_begin(!can_bake);
|
||||
if (ImGui::Button((bake_label + "##bake").c_str(), ImVec2(x0 + panel_w - ImGui::GetCursorPosX(), button_h))) {
|
||||
ImGui::SetCursorPosY(row_y + (row_h - button_h) * .5f); // center vertically
|
||||
if (ImGui::Button((bake_label + "##bake").c_str(), ImVec2(button_w, button_h))) {
|
||||
// Standard mode's Bake is the whole pipeline (remesh -> refine -> displace); Pro's is only the
|
||||
// displacement, because there the user has already prepared the mesh with the controls above.
|
||||
if (pro_mode())
|
||||
@@ -6935,6 +7031,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"land on - all in one step."),
|
||||
wrap_w);
|
||||
|
||||
ImGui::SameLine();
|
||||
if (busy) {
|
||||
if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(button_w, button_h)))
|
||||
wxGetApp().plater()->get_ui_job_worker().cancel_all();
|
||||
hover_tip(_u8L("Stops the bake. Whatever it had already finished stays on the model, and can be undone."));
|
||||
} else {
|
||||
if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(button_w, button_h)))
|
||||
m_parent.reset_all_gizmos();
|
||||
hover_tip(_u8L("Closes the tool without baking. Your paint, layers and settings stay with the model."));
|
||||
}
|
||||
|
||||
// What Bake will produce, and which layers it will skip.
|
||||
if (mv != nullptr) {
|
||||
const std::string base_count = base_k > 0 ? Slic3r::format(_u8L("%1% k"), base_k) :
|
||||
|
||||
@@ -943,6 +943,13 @@ private:
|
||||
|
||||
std::map<std::string, wxString> m_desc;
|
||||
|
||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_brush;
|
||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
|
||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
|
||||
|
||||
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
|
||||
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
|
||||
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "GLGizmosManager.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <imgui.h>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <string_view>
|
||||
#include <cstddef>
|
||||
#include <wx/app.h>
|
||||
#include <wx/utils.h>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
|
||||
@@ -55,7 +57,6 @@ namespace Slic3r::GUI::GLGizmoUtils {
|
||||
|
||||
auto& gizmos_manager = canvas.get_gizmos_manager();
|
||||
ImTextureID normal_id = gizmos_manager.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP);
|
||||
ImTextureID hover_id = gizmos_manager.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER);
|
||||
|
||||
float scale = canvas.get_scale();
|
||||
#ifdef WIN32
|
||||
@@ -63,13 +64,7 @@ namespace Slic3r::GUI::GLGizmoUtils {
|
||||
scale *= (float)dpi / (float)DPI_DEFAULT;
|
||||
#endif
|
||||
|
||||
ImVec2 button_size = ImVec2(25 * scale, 25 * scale);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 });
|
||||
|
||||
ImGui::ImageButton3(normal_id, hover_id, button_size);
|
||||
|
||||
toolbar_circular_button(normal_id, "##tooltip_btn", scale);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip2(ImVec2(x, caption_y));
|
||||
for (const auto& item : shortcuts) {
|
||||
@@ -79,7 +74,60 @@ namespace Slic3r::GUI::GLGizmoUtils {
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
bool render_wiki_guide_button(const GLCanvas3D& canvas, float scale, const char* url)
|
||||
{
|
||||
auto& gm = canvas.get_gizmos_manager();
|
||||
auto icon = gm.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_WIKI_GUIDE);
|
||||
bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale);
|
||||
if (url && *url && clicked)
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
|
||||
if (ImGui::IsItemHovered()){
|
||||
if(url && *url)
|
||||
ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url);
|
||||
else
|
||||
ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str());
|
||||
}
|
||||
return clicked; // for dynamically generated links
|
||||
}
|
||||
|
||||
bool render_video_guide_button(const GLCanvas3D& canvas, float scale, const char* url)
|
||||
{
|
||||
auto& gm = canvas.get_gizmos_manager();
|
||||
auto icon = gm.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_VIDEO_GUIDE);
|
||||
bool clicked = toolbar_circular_button(icon, "##video_guide_btn", scale);
|
||||
if (url && *url && clicked)
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
|
||||
if (ImGui::IsItemHovered()){
|
||||
if(url && *url)
|
||||
ImGui::SetTooltip("%s\n%s", _u8L("Video Guide").c_str(), url);
|
||||
else
|
||||
ImGui::SetTooltip("%s", _u8L("Video Guide").c_str());
|
||||
}
|
||||
return clicked; // for dynamically generated links
|
||||
}
|
||||
|
||||
bool toolbar_circular_button(ImTextureID textureID, const char* id, float scale)
|
||||
{
|
||||
ImVec2 btn_sz = ImVec2(21 * scale, 21 * scale);
|
||||
ImVec2 icon_sz = ImVec2(15 * scale, 15 * scale);
|
||||
float btn_pad = (btn_sz.x - icon_sz.x) * .5f;
|
||||
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
bool clicked = ImGui::InvisibleButton(id, btn_sz);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
bool is_dark = ImGuiWrapper::COL_WINDOW_BG.x != ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).x;
|
||||
ImVec4 col = is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA;
|
||||
ImVec4 col_hvr = is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER;
|
||||
dl->AddCircleFilled(
|
||||
ImVec2(p.x + btn_sz.x * .5f, p.y + btn_sz.y * .5f),
|
||||
btn_sz.x * .5f,
|
||||
ImGui::ColorConvertFloat4ToU32((ImGui::IsItemActive()||ImGui::IsItemHovered()) ? col_hvr : col)
|
||||
);
|
||||
dl->AddImage(textureID, ImVec2(p.x + btn_pad, p.y + btn_pad), ImVec2(p.x + btn_sz.x - btn_pad, p.y + btn_sz.y - btn_pad));
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
||||
void begin_right_aligned_buttons(const std::vector<wxString>& labels)
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace GLGizmoUtils {
|
||||
// Renders a tooltip button using the provided shortcuts
|
||||
void render_tooltip_button(
|
||||
ImGuiWrapper* imgui_wrapper, const GLCanvas3D& canvas, const std::vector<std::pair<wxString, wxString>>& shortcuts, float x, float y);
|
||||
|
||||
void render_wiki_buttons(
|
||||
ImGuiWrapper* imgui_wrapper, const GLCanvas3D& canvas, const char* wiki_url, const char* video_url = "");
|
||||
bool render_wiki_guide_button(const GLCanvas3D& canvas, float scale, const char* url = "");
|
||||
bool render_video_guide_button(const GLCanvas3D& canvas, float scale, const char* url = "");
|
||||
|
||||
// Sets up ImGui to render buttons that are right-aligned within the current window, using the provided labels to calculate spacing.
|
||||
void begin_right_aligned_buttons(const std::vector<wxString>& labels);
|
||||
@@ -27,6 +32,8 @@ namespace GLGizmoUtils {
|
||||
|
||||
void pop_orca_button_style();
|
||||
|
||||
bool toolbar_circular_button(ImTextureID textureID, const char* id, float scale);
|
||||
|
||||
} // namespace GLGizmoUtils
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
|
||||
@@ -308,13 +308,18 @@ bool GLGizmosManager::init_icon_textures()
|
||||
else
|
||||
return false;
|
||||
|
||||
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip.svg", 25, 25, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon
|
||||
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip.svg", 15, 15, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon
|
||||
icon_list.insert(std::make_pair((int)IC_TOOLBAR_TOOLTIP, texture_id));
|
||||
else
|
||||
return false;
|
||||
|
||||
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip_hover.svg", 25, 25, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon
|
||||
icon_list.insert(std::make_pair((int)IC_TOOLBAR_TOOLTIP_HOVER, texture_id));
|
||||
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_wiki.svg", 15, 15, texture_id))
|
||||
icon_list.insert(std::make_pair((int)IC_TOOLBAR_WIKI_GUIDE, texture_id));
|
||||
else
|
||||
return false;
|
||||
|
||||
if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_video_guide.svg", 15, 15, texture_id))
|
||||
icon_list.insert(std::make_pair((int)IC_TOOLBAR_VIDEO_GUIDE, texture_id));
|
||||
else
|
||||
return false;
|
||||
|
||||
|
||||
@@ -182,7 +182,8 @@ public:
|
||||
IC_TOOLBAR_RESET_ZERO,
|
||||
IC_TOOLBAR_RESET_ZERO_HOVER,
|
||||
IC_TOOLBAR_TOOLTIP,
|
||||
IC_TOOLBAR_TOOLTIP_HOVER,
|
||||
IC_TOOLBAR_WIKI_GUIDE,
|
||||
IC_TOOLBAR_VIDEO_GUIDE,
|
||||
IC_NAME_COUNT,
|
||||
IC_CANVAS_MENU,
|
||||
IC_CANVAS_MENU_HOVER,
|
||||
|
||||
@@ -2309,6 +2309,12 @@ wxBoxSizer* MainFrame::create_side_tools()
|
||||
|
||||
bool slice = true;
|
||||
|
||||
// The Slice-plate hover popup is a transient popup that keeps grabbing
|
||||
// the mouse capture while shown. Left behind the modal grouping dialog it
|
||||
// would starve that dialog of mouse events, so close it synchronously first.
|
||||
if (m_filament_group_popup)
|
||||
m_filament_group_popup->Dismiss();
|
||||
|
||||
auto curr_plate = m_plater->get_partplate_list().get_curr_plate();
|
||||
#ifdef __linux__
|
||||
PresetBundle* preset = wxGetApp().preset_bundle;
|
||||
|
||||
@@ -3128,7 +3128,7 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) {
|
||||
new_conf.set_key_value("detect_thin_wall", new ConfigOptionBool(false));
|
||||
new_conf.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
new_conf.set_key_value("overhang_reverse", new ConfigOptionBool(false));
|
||||
auto applying_keys = global_config->diff(new_conf);
|
||||
const auto applying_keys = global_config->diff(new_conf);
|
||||
|
||||
for (ModelObject* object : obj_ptrs) {
|
||||
ModelConfigObject& config = object->config;
|
||||
@@ -3137,8 +3137,8 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) {
|
||||
config.set_key_value(opt_key, new_conf.option(opt_key)->clone());
|
||||
}
|
||||
|
||||
applying_keys = config.get().diff(new_conf);
|
||||
for (auto opt_key : applying_keys) {
|
||||
const auto object_keys = config.get().diff(new_conf);
|
||||
for (auto opt_key : object_keys) {
|
||||
config.set_key_value(opt_key, new_conf.option(opt_key)->clone());
|
||||
}
|
||||
}
|
||||
|
||||
+33
-28
@@ -8800,6 +8800,20 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st
|
||||
return;
|
||||
}
|
||||
|
||||
// Logs what show_substitutions_info() would list, for loads that don't show the dialog.
|
||||
static void log_substitutions(const ConfigSubstitutions& substitutions, const std::string& source)
|
||||
{
|
||||
for (const ConfigSubstitution& substitution : substitutions)
|
||||
BOOST_LOG_TRIVIAL(warning) << "Loading " << source << ": " << substitution.opt_def->opt_key << " = \"" << substitution.old_value
|
||||
<< "\" replaced with \"" << substitution.new_value->serialize() << "\"";
|
||||
}
|
||||
|
||||
static void log_substitutions(const PresetsConfigSubstitutions& substitutions, const std::string& source)
|
||||
{
|
||||
for (const PresetConfigSubstitutions& preset : substitutions)
|
||||
log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")");
|
||||
}
|
||||
|
||||
// BBS: backup & restore
|
||||
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
|
||||
LoadStrategy strategy,
|
||||
@@ -9101,7 +9115,6 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
Semver app_version = *(Semver::parse(SoftFever_VERSION));
|
||||
const wxString load_3mf_title = _L("Load 3MF");
|
||||
const wxString newer_3mf_title = _L("Newer 3MF version");
|
||||
const wxString bambu_project_title = _L("BambuStudio Project");
|
||||
const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only.");
|
||||
const wxString msg_old_orca_geometry = _L("The 3MF file was generated by an old OrcaSlicer version, loading geometry data only.");
|
||||
const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only.");
|
||||
@@ -9112,6 +9125,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
<< boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string();
|
||||
show_info(q, text, title);
|
||||
};
|
||||
// Untagged files up to 2.3.2 may also come from OrcaSlicer, which only started tagging its 3MFs after it.
|
||||
const bool is_bambu_studio_project = en_3mf_file_type == En3mfType::From_BBS && file_version > Semver(2, 3, 2);
|
||||
if (en_3mf_file_type == En3mfType::From_Prusa) {
|
||||
// do not reset the model config
|
||||
load_config = false;
|
||||
@@ -9171,8 +9186,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
}
|
||||
else if (en_3mf_file_type == En3mfType::From_BBS) {
|
||||
// No OrcaSlicer tag - check Bambu/Application version
|
||||
Semver orca_tag_start_version(2, 3, 2);
|
||||
if (file_version <= orca_tag_start_version) {
|
||||
if (!is_bambu_studio_project) {
|
||||
// Compatible old version (before OrcaSlicer tagging was introduced after 2.3.2).
|
||||
// Any version prior or equal to 2.3.2 is older than the current one, no version warnings needed.
|
||||
// Still apply migration fixes for known old versions.
|
||||
@@ -9205,33 +9219,17 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
}
|
||||
} else {
|
||||
// BambuStudio project (version > 2.3.2 without OrcaSlicer tag)
|
||||
// Report that a BambuStudio project is being imported and compare with SLIC3R_VERSION
|
||||
Semver slic3r_version = *(Semver::parse(SLIC3R_VERSION));
|
||||
if (load_config && config_loaded.empty()) {
|
||||
load_config = false;
|
||||
log_and_show_3mf_info(msg_bambu_geometry, load_3mf_title);
|
||||
}
|
||||
else if (load_config && (file_version > slic3r_version)) {
|
||||
// BambuStudio file version is newer than our compatible SLIC3R_VERSION
|
||||
if (config_substitutions.unrecogized_keys.size() > 0) {
|
||||
wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:"),
|
||||
file_version.to_string(), slic3r_version.to_string());
|
||||
text += "\n";
|
||||
wxString context = text;
|
||||
wxString append = _L("You should update your software.\n");
|
||||
context += "\n\n";
|
||||
context += append;
|
||||
log_and_show_3mf_info(context, bambu_project_title);
|
||||
} else {
|
||||
wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible."),
|
||||
file_version.to_string(), slic3r_version.to_string());
|
||||
text += "\n";
|
||||
log_and_show_3mf_info(text, bambu_project_title);
|
||||
}
|
||||
} else if (load_config && !published_config.published) {
|
||||
// BambuStudio version is older or same as our SLIC3R_VERSION
|
||||
wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer.");
|
||||
log_and_show_3mf_info(text, bambu_project_title);
|
||||
else if (load_config) {
|
||||
// Logged, not shown: it is the same for every BambuStudio project and needs no action.
|
||||
std::string unrecognized;
|
||||
for (const std::string& key : config_substitutions.unrecogized_keys)
|
||||
unrecognized += (unrecognized.empty() ? "" : ", ") + key;
|
||||
BOOST_LOG_TRIVIAL(info) << "BambuStudio " << file_version.to_string() << " project " << path.string()
|
||||
<< ", unrecognized settings: " << (unrecognized.empty() ? "none" : unrecognized);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9320,7 +9318,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
PresetsConfigSubstitutions preset_substitutions;
|
||||
PresetBundle & preset_bundle = *wxGetApp().preset_bundle;
|
||||
preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable);
|
||||
if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions);
|
||||
if (is_bambu_studio_project)
|
||||
log_substitutions(preset_substitutions, path.string());
|
||||
else if (!preset_substitutions.empty())
|
||||
show_substitutions_info(preset_substitutions);
|
||||
}
|
||||
if (project_presets.size() > 0) {
|
||||
for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; }
|
||||
@@ -9358,7 +9359,11 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
notify_manager->bbl_show_3mf_warn_notification(error_message);
|
||||
}
|
||||
}
|
||||
if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string());
|
||||
// BambuStudio projects routinely carry values Orca replaces; log them rather than showing a dialog on every open.
|
||||
if (is_bambu_studio_project)
|
||||
log_substitutions(config_substitutions.substitutions, path.string());
|
||||
else if (!config_substitutions.empty())
|
||||
show_substitutions_info(config_substitutions.substitutions, filename.string());
|
||||
|
||||
// BBS
|
||||
if (load_model && !load_config) {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include <wx/sizer.h>
|
||||
#include "slic3r/GUI/Widgets/HyperLink.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "libslic3r/CommonDefs.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
@@ -1545,12 +1544,14 @@ PrinterPartsDialog::PrinterPartsDialog(wxWindow* parent)
|
||||
change_nozzle_tips->SetFont(Label::Body_13);
|
||||
change_nozzle_tips->SetForegroundColour(STATIC_TEXT_CAPTION_COL);
|
||||
|
||||
m_wiki_link = new HyperLink(single_panel, _L("Wiki Guide")); // ORCA
|
||||
m_wiki_link->SetFont(Label::Body_13);
|
||||
m_wiki_link = new Button(single_panel, "", "toolbar_wiki", 0, 15);
|
||||
m_wiki_link->SetToolTip(_L("Wiki Guide"));
|
||||
m_wiki_link->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
m_wiki_link->SetCanFocus(false);
|
||||
m_wiki_link->Bind(wxEVT_LEFT_DOWN, &PrinterPartsDialog::OnWikiClicked, this);
|
||||
|
||||
h_tips_sizer->Add(change_nozzle_tips, 0, wxLEFT);
|
||||
h_tips_sizer->Add(m_wiki_link, 0, wxLEFT, FromDIP(5));
|
||||
h_tips_sizer->Add(m_wiki_link);
|
||||
h_tips_sizer->Add(change_nozzle_tips, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5));
|
||||
|
||||
wxSizer* single_update_nozzle_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_single_update_nozzle_button = new Button(single_panel, _L("Refresh"));
|
||||
@@ -1656,13 +1657,15 @@ PrinterPartsDialog::PrinterPartsDialog(wxWindow* parent)
|
||||
multiple_change_nozzle_tips->SetFont(Label::Body_13);
|
||||
multiple_change_nozzle_tips->SetForegroundColour(STATIC_TEXT_CAPTION_COL);
|
||||
|
||||
multiple_wiki_link = new HyperLink(multiple_panel, _L("Wiki Guide")); // ORCA
|
||||
multiple_wiki_link->SetFont(Label::Body_13);
|
||||
multiple_wiki_link = new Button(multiple_panel, "", "toolbar_wiki", 0, 15);
|
||||
multiple_wiki_link->SetToolTip(_L("Wiki Guide"));
|
||||
multiple_wiki_link->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
multiple_wiki_link->SetCanFocus(false);
|
||||
multiple_wiki_link->Bind(wxEVT_LEFT_DOWN, &PrinterPartsDialog::OnWikiClicked, this);
|
||||
|
||||
wxSizer* multiple_change_tips_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
multiple_change_tips_sizer->Add(multiple_change_nozzle_tips, 0, wxLEFT);
|
||||
multiple_change_tips_sizer->Add(multiple_wiki_link, 0, wxLEFT, FromDIP(5));
|
||||
multiple_change_tips_sizer->Add(multiple_wiki_link);
|
||||
multiple_change_tips_sizer->Add(multiple_change_nozzle_tips, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5));
|
||||
|
||||
wxSizer* multiple_update_nozzle_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_multiple_update_nozzle_button = new Button(multiple_panel, _L("Refresh"));
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/StaticLine.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
|
||||
// Previous definitions
|
||||
class Button;
|
||||
class SwitchBoard;
|
||||
class MultiSwitchButton;
|
||||
|
||||
@@ -41,7 +41,7 @@ protected:
|
||||
Label* nozzle_flow_type_label;
|
||||
ComboBox* nozzle_flow_type_checkbox;
|
||||
Label *change_nozzle_tips;
|
||||
HyperLink* m_wiki_link;
|
||||
Button* m_wiki_link;
|
||||
Button* m_single_update_nozzle_button;
|
||||
Button* m_multiple_update_nozzle_button;
|
||||
|
||||
@@ -54,7 +54,7 @@ protected:
|
||||
ComboBox *multiple_right_nozzle_flow_checkbox;
|
||||
|
||||
Label *multiple_change_nozzle_tips;
|
||||
HyperLink* multiple_wiki_link;
|
||||
Button* multiple_wiki_link;
|
||||
|
||||
wxPanel *single_panel;
|
||||
wxPanel *multiple_panel;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#include "ConfigValueFormatter.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/DialogButtons.hpp"
|
||||
#include "Widgets/StaticLine.hpp"
|
||||
@@ -756,11 +756,24 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
|
||||
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||||
|
||||
// Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right).
|
||||
wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
auto* wiki_link = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html");
|
||||
auto* video_link = new HyperLink(this, _L("Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg");
|
||||
links_sizer->Add(wiki_link , 0, wxALIGN_LEFT);
|
||||
links_sizer->Add(video_link, 0, wxTOP | wxALIGN_LEFT, FromDIP(4));
|
||||
wxBoxSizer* links_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
|
||||
auto video_url = "https://www.youtube.com/watch?v=-xt1N29UIOg";
|
||||
video_btn->SetToolTip(_L("Video Guide") + "\n" + video_url);
|
||||
video_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
video_btn->SetCanFocus(false);
|
||||
video_btn->Bind(wxEVT_LEFT_DOWN, ([video_url](auto& e) {wxLaunchDefaultBrowser(video_url);}));
|
||||
|
||||
links_sizer->Add(wiki_btn , 0, wxLEFT, FromDIP(10));
|
||||
links_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
|
||||
|
||||
wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL);
|
||||
footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL);
|
||||
@@ -771,7 +784,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
|
||||
footer_line->SetMinSize(wxSize(-1, 1));
|
||||
footer_line->SetMaxSize(wxSize(-1, 1));
|
||||
w_sizer->Add(footer_line, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
|
||||
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxTOP | wxBOTTOM | wxEXPAND, FromDIP(10));
|
||||
w_sizer->Add(footer, 0, wxEXPAND, FromDIP(10));
|
||||
|
||||
SetSizerAndFit(w_sizer);
|
||||
fit_to_content(); // initial size only; the dialog is resizable
|
||||
|
||||
@@ -113,6 +113,7 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
|
||||
SHORTCUT(GizmoMeasure, "gizmo_measure", L("Gizmo measure"), PLATER, { 'U' }),
|
||||
SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }),
|
||||
SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }),
|
||||
SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }),
|
||||
|
||||
// Sliders
|
||||
SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }),
|
||||
|
||||
@@ -38,7 +38,7 @@ enum class Shortcut : uint8_t {
|
||||
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
|
||||
// Gizmos
|
||||
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
|
||||
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars,
|
||||
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement,
|
||||
// Sliders
|
||||
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
|
||||
// Painting tools
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include "slic3r/GUI/Widgets/HyperLink.hpp"
|
||||
#include "slic3r/GUI/MsgDialog.hpp"
|
||||
#include "libslic3r/LocalesUtils.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
@@ -26,6 +25,7 @@
|
||||
#include <chrono>
|
||||
#include <wx/string.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/valtext.h>
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -131,19 +131,14 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line
|
||||
|
||||
auto tip_frame = new RoundedRectangle(this, StateColor::darkModeColorFor(wxColour("#F1F1F1")), wxDefaultPosition, wxSize(-1,-1), 6, 0);
|
||||
|
||||
wxBoxSizer* tips_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
wxBoxSizer* tips_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
wxStaticText* info = new wxStaticText(tip_frame, wxID_ANY, _L("Smaller linear and angular deflections result in higher-quality transformations but increase the processing time."));
|
||||
info->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
info->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F1F1F1")));
|
||||
info->SetFont(::Label::Body_14);
|
||||
info->Wrap(FromDIP(450));
|
||||
|
||||
// ORCA standardized HyperLink
|
||||
HyperLink *tips = new HyperLink(tip_frame, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/import_export#step");
|
||||
tips->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F1F1F1")));
|
||||
|
||||
tips_sizer->Add(info, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(10));
|
||||
tips_sizer->Add(tips, 0, wxALL, FromDIP(10));
|
||||
tips_sizer->Add(info, 0, wxEXPAND |wxALL , FromDIP(10));
|
||||
|
||||
tip_frame->SetSizer(tips_sizer);
|
||||
tip_frame->Layout();
|
||||
@@ -289,12 +284,24 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line
|
||||
save_default_sizer->Add(m_save_default_checkbox, 0, wxALIGN_LEFT);
|
||||
bSizer->Add(save_default_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, LEFT_RIGHT_PADING);
|
||||
|
||||
wxBoxSizer* bSizer_button = new wxBoxSizer(wxHORIZONTAL);
|
||||
bSizer_button->SetMinSize(wxSize(FromDIP(100), -1));
|
||||
bSizer->AddSpacer(FromDIP(10));
|
||||
|
||||
m_checkbox = new wxCheckBox(this, wxID_ANY, _L("Don't show again"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_checkbox->SetFont(::Label::Body_14);
|
||||
m_checkbox->SetForegroundColour(StateColor::darkModeColorFor(FONT_COLOR));
|
||||
bSizer_button->Add(m_checkbox, 0, wxALIGN_LEFT | wxLEFT | wxALIGN_CENTER_VERTICAL, LEFT_RIGHT_PADING);
|
||||
bSizer->Add(m_checkbox, 0, wxLEFT, LEFT_RIGHT_PADING);
|
||||
|
||||
wxBoxSizer* bSizer_button = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
// ORCA standardized HyperLink
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/import_export#step";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
bSizer_button->Add(wiki_btn, 0, wxALIGN_LEFT | wxLEFT | wxALIGN_CENTER_VERTICAL, LEFT_RIGHT_PADING);
|
||||
|
||||
bSizer_button->AddStretchSpacer(1);
|
||||
|
||||
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
|
||||
|
||||
@@ -236,7 +236,19 @@ TroubleshootDialog::TroubleshootDialog()
|
||||
Fit();
|
||||
});
|
||||
|
||||
auto link_wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/troubleshoot_center");
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/troubleshoot_center";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
|
||||
auto video_url = "https://www.youtube.com/watch?v=CFzt8W7OCx0";
|
||||
video_btn->SetToolTip(_L("Video Guide") + "\n" + video_url);
|
||||
video_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
video_btn->SetCanFocus(false);
|
||||
video_btn->Bind(wxEVT_LEFT_DOWN, ([video_url](auto& e) {wxLaunchDefaultBrowser(video_url);}));
|
||||
|
||||
// RIGHT SIZER //////////////////////
|
||||
|
||||
@@ -367,6 +379,10 @@ TroubleshootDialog::TroubleshootDialog()
|
||||
sys_btn_sizer->AddStretchSpacer();
|
||||
sys_btn_sizer->Add(sys_copy_btn, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
|
||||
wxBoxSizer *link_btn_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
link_btn_sizer->Add(wiki_btn);
|
||||
link_btn_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
|
||||
|
||||
left_sizer->Add(m_header_logo , 0, wxEXPAND | wxALIGN_CENTER);
|
||||
left_sizer->Add(logo_line , 0, wxEXPAND | wxTOP, FromDIP(12));
|
||||
left_sizer->Add(version , 0, wxEXPAND | wxTOP, FromDIP(6));
|
||||
@@ -374,8 +390,7 @@ TroubleshootDialog::TroubleshootDialog()
|
||||
left_sizer->Add(sys_panel , 0, wxEXPAND | wxTOP, FromDIP(15));
|
||||
left_sizer->AddStretchSpacer();
|
||||
left_sizer->Add(sys_btn_sizer , 0, wxEXPAND | wxTOP, FromDIP(15));
|
||||
left_sizer->Add(link_wiki , 0, wxALIGN_CENTER | wxTOP, FromDIP(15));
|
||||
left_sizer->AddSpacer(FromDIP(5));
|
||||
left_sizer->Add(link_btn_sizer , 0, wxALIGN_CENTER | wxTOP, FromDIP(15));
|
||||
|
||||
wxBoxSizer *right_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ UVEditorCanvas::UVEditorCanvas(wxWindow *parent)
|
||||
Bind(wxEVT_MIDDLE_DOWN, &UVEditorCanvas::on_mouse, this);
|
||||
Bind(wxEVT_MIDDLE_UP, &UVEditorCanvas::on_mouse, this);
|
||||
Bind(wxEVT_MOTION, &UVEditorCanvas::on_mouse, this);
|
||||
Bind(wxEVT_MOUSE_CAPTURE_LOST, &UVEditorCanvas::on_capture_lost, this);
|
||||
Bind(wxEVT_MOUSEWHEEL, &UVEditorCanvas::on_mouse, this);
|
||||
Bind(wxEVT_LEAVE_WINDOW, &UVEditorCanvas::on_leave, this);
|
||||
Bind(wxEVT_KEY_DOWN, &UVEditorCanvas::on_key, this);
|
||||
@@ -750,10 +751,47 @@ void UVEditorCanvas::end_gesture()
|
||||
m_rot_raw_deg = 0.f;
|
||||
m_rot_applied_deg = 0.f;
|
||||
m_modal_scale_accum = 1.f;
|
||||
drop_mouse();
|
||||
}
|
||||
|
||||
void UVEditorCanvas::cancel_gesture()
|
||||
{
|
||||
// Undo what the gesture already applied live, as the Esc path does, and commit nothing.
|
||||
if (m_on_island_edit) {
|
||||
if (m_gesture == Gesture::RotateIslandModal && m_rot_applied_deg != 0.f)
|
||||
m_on_island_edit(m_selected_island, Vec2f::Zero(), -m_rot_applied_deg, 1.f, false);
|
||||
if (m_gesture == Gesture::ScaleIslandModal && m_modal_scale_accum != 1.f)
|
||||
m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f / m_modal_scale_accum, false);
|
||||
}
|
||||
|
||||
m_gesture = Gesture::None;
|
||||
m_rot_raw_deg = 0.f;
|
||||
m_rot_applied_deg = 0.f;
|
||||
m_modal_scale_accum = 1.f;
|
||||
m_vertex_edit_moved = false;
|
||||
}
|
||||
|
||||
void UVEditorCanvas::grab_mouse()
|
||||
{
|
||||
if (!HasCapture())
|
||||
CaptureMouse();
|
||||
}
|
||||
|
||||
void UVEditorCanvas::drop_mouse()
|
||||
{
|
||||
if (HasCapture())
|
||||
ReleaseMouse();
|
||||
}
|
||||
|
||||
// The capture was taken from us (a dialog opened, another application grabbed the pointer). wx
|
||||
// requires this to cancel the gesture: no commit, no Skip(), and no ReleaseMouse() - the capture is
|
||||
// already gone, and releasing it again would unbalance the stack.
|
||||
void UVEditorCanvas::on_capture_lost(wxMouseCaptureLostEvent &)
|
||||
{
|
||||
cancel_gesture();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void UVEditorCanvas::on_key(wxKeyEvent &evt)
|
||||
{
|
||||
const int key = evt.GetKeyCode();
|
||||
@@ -932,7 +970,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
|
||||
}
|
||||
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
|
||||
}
|
||||
CaptureMouse();
|
||||
grab_mouse();
|
||||
Refresh();
|
||||
} else if (type == wxEVT_RIGHT_DOWN && m_selected_island >= 0 && m_select_mode == SelectMode::Island) {
|
||||
const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island);
|
||||
@@ -942,12 +980,16 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
|
||||
m_rot_base_deg = island_rotation_deg(m_selected_island);
|
||||
m_rot_display_deg = m_rot_base_deg;
|
||||
m_gesture_last_angle = std::atan2(rel.y(), rel.x());
|
||||
CaptureMouse();
|
||||
grab_mouse();
|
||||
} else if (type == wxEVT_MIDDLE_DOWN) {
|
||||
m_gesture = Gesture::Pan;
|
||||
m_drag_last_px = pos;
|
||||
CaptureMouse();
|
||||
grab_mouse();
|
||||
} else if (type == wxEVT_LEFT_UP || type == wxEVT_RIGHT_UP || type == wxEVT_MIDDLE_UP) {
|
||||
// The drag is over either way. A modal R/S keeps running until a click confirms it, but it
|
||||
// tracks the pointer over this canvas and needs no capture to do so, so the capture goes back
|
||||
// here rather than waiting for that click - which may never come.
|
||||
drop_mouse();
|
||||
if (m_gesture != Gesture::RotateIslandModal && m_gesture != Gesture::ScaleIslandModal) {
|
||||
end_gesture();
|
||||
Refresh();
|
||||
@@ -1735,7 +1777,7 @@ public:
|
||||
bool toggle, bool accent = false, int size_dip = 26)
|
||||
: wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFULL_REPAINT_ON_RESIZE)
|
||||
, m_icon_name(icon), m_icon_dip(size_dip >= 26 ? 16 : 14), m_label(label), m_toggle(toggle), m_accent(accent)
|
||||
, m_size_dip(size_dip)
|
||||
, m_size_dip(size_dip), m_tip(tip)
|
||||
{
|
||||
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
SetToolTip(tip);
|
||||
@@ -1748,7 +1790,7 @@ public:
|
||||
Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent &) { m_hover = true; Refresh(); });
|
||||
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &) { m_hover = false; m_pressed = false; Refresh(); });
|
||||
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) {
|
||||
if (IsEnabled()) {
|
||||
if (usable()) {
|
||||
m_pressed = true;
|
||||
Refresh();
|
||||
}
|
||||
@@ -1757,7 +1799,7 @@ public:
|
||||
const bool was_pressed = m_pressed;
|
||||
m_pressed = false;
|
||||
Refresh();
|
||||
if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition()))
|
||||
if (!was_pressed || !usable() || !GetClientRect().Contains(e.GetPosition()))
|
||||
return;
|
||||
if (m_toggle)
|
||||
m_on = !m_on;
|
||||
@@ -1797,6 +1839,30 @@ public:
|
||||
Refresh();
|
||||
return changed;
|
||||
}
|
||||
// Soft-disable: the button is drawn faded and swallows clicks, but stays a live window, so hovering it
|
||||
// still raises its tooltip - now with `reason` appended, saying what to do to make it usable. A window
|
||||
// really disabled with Enable(false) gets no mouse events at all on GTK and MSW, which leaves the user
|
||||
// guessing; this is the same trade-off the gizmo panel's icon_toggle() makes with its `unavailable`.
|
||||
// An empty reason makes the button usable again.
|
||||
void SetUnavailable(const wxString &reason)
|
||||
{
|
||||
if (reason == m_unavailable)
|
||||
return;
|
||||
m_unavailable = reason;
|
||||
SetToolTip(reason.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + reason);
|
||||
if (!m_unavailable.empty())
|
||||
m_pressed = false; // a reason appearing mid-press cancels the press
|
||||
Refresh();
|
||||
}
|
||||
// Replaces the plain tooltip, keeping whatever reason is currently appended to it.
|
||||
void SetTip(const wxString &tip)
|
||||
{
|
||||
if (tip == m_tip)
|
||||
return;
|
||||
m_tip = tip;
|
||||
SetToolTip(m_unavailable.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + m_unavailable);
|
||||
}
|
||||
bool usable() const { return IsEnabled() && m_unavailable.empty(); }
|
||||
|
||||
protected:
|
||||
wxSize DoGetBestSize() const override
|
||||
@@ -1815,7 +1881,7 @@ private:
|
||||
const PaneColors c = PaneColors::current();
|
||||
const wxRect r = GetClientRect();
|
||||
const wxColour teal(0x00, 0x96, 0x88);
|
||||
const bool enabled = IsEnabled();
|
||||
const bool enabled = usable();
|
||||
|
||||
wxColour fill = c.bg, border = c.frame, text = c.ink;
|
||||
if (m_accent) {
|
||||
@@ -1875,6 +1941,8 @@ private:
|
||||
bool m_toggle = false;
|
||||
bool m_accent = false;
|
||||
int m_size_dip = 26;
|
||||
wxString m_tip; // the tooltip without any m_unavailable reason appended
|
||||
wxString m_unavailable; // non-empty: faded and unclickable, and why (see SetUnavailable())
|
||||
bool m_on = false;
|
||||
bool m_badge = false;
|
||||
bool m_hover = false;
|
||||
@@ -1920,6 +1988,9 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
});
|
||||
m_layer_name->SetMinSize(wxSize(FromDIP(30), -1));
|
||||
m_tile = text(wxEmptyString, c.dim);
|
||||
m_tile->SetToolTip(_L("The active layer's tile size: how much of the model one repeat of the texture covers. The "
|
||||
"canvas is measured in tiles, so one grid cell is one repeat. Change it with Tiling in the "
|
||||
"layer's settings."));
|
||||
header->Add(m_thumb, 0, wxALIGN_CENTER_VERTICAL);
|
||||
header->Add(m_layer_name, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
|
||||
header->Add(m_tile, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
|
||||
@@ -1973,9 +2044,19 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
strip->Add(r, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(11));
|
||||
strip->AddSpacer(FromDIP(7));
|
||||
};
|
||||
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", _L("Island - move, rotate and scale whole islands"), true);
|
||||
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", _L("Vertex - drag vertices to reshape; Shift/Ctrl to multi-select"), true);
|
||||
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", _L("Edge - drag edges to reshape; Shift/Ctrl to multi-select"), true);
|
||||
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island",
|
||||
_L("Island - work on whole islands. Click one to select it, then drag to move it, right-drag to "
|
||||
"rotate it, or press R to rotate and S to scale with the mouse (click or Enter to confirm, Esc "
|
||||
"to cancel)."),
|
||||
true);
|
||||
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex",
|
||||
_L("Vertex - drag vertices to reshape an island by hand; Shift adds to the selection, Ctrl "
|
||||
"toggles one in or out of it."),
|
||||
true);
|
||||
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge",
|
||||
_L("Edge - drag edges to reshape an island by hand; Shift adds to the selection, Ctrl toggles "
|
||||
"one in or out of it."),
|
||||
true);
|
||||
strip_rule();
|
||||
m_mark_seams = tool(ID_UV_MARK_SEAMS, "texture_displacement_uv_seam",
|
||||
_L("Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is "
|
||||
@@ -1996,7 +2077,11 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
m_clear_edits = tool(ID_UV_CLEAR_EDITS, "texture_displacement_uv_clear_edits",
|
||||
_L("Clear UV edits - discard all manual vertex/edge moves and return the unwrap to its automatic shape"), false);
|
||||
m_snap = tool(ID_UV_SNAP, "texture_displacement_uv_snap", _L("Snap - stick islands together when dragging one against another"), true);
|
||||
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", _L("Frame all islands (Home)"), false);
|
||||
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame",
|
||||
_L("Frame all islands, fitting every one of them in view (Home or F).\n"
|
||||
"Elsewhere on the canvas: scroll to zoom around the cursor, and middle-drag - or drag empty space - "
|
||||
"to pan."),
|
||||
false);
|
||||
strip->AddSpacer(FromDIP(4));
|
||||
|
||||
m_canvas = new UVEditorCanvas(this);
|
||||
@@ -2007,6 +2092,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
|
||||
// ---- status line: the current gesture on the left, the unwrap summary on the right ----
|
||||
m_status = text(wxEmptyString, c.dim, wxST_ELLIPSIZE_END);
|
||||
m_status->SetToolTip(_L("What is selected, and the exact figures of the move, rotation or scale while you drag one."));
|
||||
m_status->SetMinSize(wxSize(FromDIP(40), -1));
|
||||
m_stats = text(wxEmptyString, c.dim);
|
||||
auto *status = new wxBoxSizer(wxHORIZONTAL);
|
||||
@@ -2082,19 +2168,25 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||
} else {
|
||||
m_thumb->SetBitmap(wxNullBitmap);
|
||||
}
|
||||
m_thumb->Enable(s.has_layer);
|
||||
// Every tool that cannot be used right now is faded with the reason appended to its tooltip, rather than
|
||||
// being hard-disabled (which would hide the tooltip too - see UVToolButton::SetUnavailable()).
|
||||
const wxString no_layer = s.has_layer ? wxString() :
|
||||
_L("The pane follows the active texture layer, and that layer has to be mapped "
|
||||
"with Unwrap (LSCM). Add a layer and set its Mapping to Unwrap.");
|
||||
m_thumb->SetUnavailable(no_layer);
|
||||
m_layer_name->SetToolTip(s.has_layer ? m_thumb->GetToolTipText() : no_layer);
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
m_background[i]->SetValue(int(s.background) == i);
|
||||
m_background[i]->Enable(s.has_layer);
|
||||
m_background[i]->SetUnavailable(no_layer);
|
||||
}
|
||||
m_unwrap->Enable(s.has_layer);
|
||||
m_unwrap->SetUnavailable(no_layer);
|
||||
m_unwrap->SetBadge(s.unwrap_stale);
|
||||
m_unwrap->SetToolTip(s.unwrap_stale ?
|
||||
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
|
||||
"Press to unwrap again.") :
|
||||
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
|
||||
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
|
||||
m_unwrap->SetTip(s.unwrap_stale ?
|
||||
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
|
||||
"Press to unwrap again.") :
|
||||
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
|
||||
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
|
||||
|
||||
if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg)))
|
||||
m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg)));
|
||||
@@ -2103,15 +2195,26 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||
m_connect->Enable(s.has_layer);
|
||||
|
||||
m_mark_seams->SetValue(s.mark_seams);
|
||||
m_mark_seams->Enable(s.has_layer);
|
||||
m_mark_seams->SetUnavailable(no_layer);
|
||||
m_seam_path->SetValue(s.seam_path);
|
||||
m_seam_path->Enable(s.has_layer && s.mark_seams);
|
||||
m_clear_seams->Enable(s.has_layer && s.has_seams);
|
||||
m_clear_edits->Enable(s.has_layer && s.has_uv_edits);
|
||||
m_seam_path->SetUnavailable(!no_layer.empty() ? no_layer :
|
||||
s.mark_seams ? wxString() :
|
||||
_L("Turn Mark seams on first - Path is a quicker way of marking them."));
|
||||
m_clear_seams->SetUnavailable(!no_layer.empty() ? no_layer :
|
||||
s.has_seams ? wxString() :
|
||||
_L("No seams are marked on this layer."));
|
||||
m_clear_edits->SetUnavailable(!no_layer.empty() ? no_layer :
|
||||
s.has_uv_edits ? wxString() :
|
||||
_L("No islands have been reshaped by hand, so there is nothing to "
|
||||
"discard."));
|
||||
|
||||
m_stats->SetLabel(s.unwrapped ? wxString::Format(_L("%d islands, %s faces"), s.island_count,
|
||||
wxString(std::to_string(s.face_count))) :
|
||||
wxString());
|
||||
m_stats->SetToolTip(s.unwrapped ? _L("How the painted area came out of the unwrap: the number of separate pieces it "
|
||||
"was cut into (at the seams and at edges sharper than the seam angle), and how "
|
||||
"many triangles they hold in total.") :
|
||||
wxString());
|
||||
refresh_selection_tools();
|
||||
if (relayout)
|
||||
Layout();
|
||||
@@ -2121,19 +2224,24 @@ void UVEditorPanel::refresh_selection_tools()
|
||||
{
|
||||
const bool has_islands = m_canvas->has_islands();
|
||||
const int mode = int(m_canvas->select_mode());
|
||||
// Faded rather than hard-disabled, so the tooltip still says what is missing (see apply_state()).
|
||||
const wxString not_unwrapped = has_islands ? wxString() : _L("Press Unwrap first - there are no islands to work on yet.");
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
m_select[i]->SetValue(i == mode);
|
||||
m_select[i]->Enable(has_islands);
|
||||
m_select[i]->SetUnavailable(not_unwrapped);
|
||||
}
|
||||
const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island &&
|
||||
m_canvas->selected_island() >= 0;
|
||||
m_avg_scale->Enable(has_islands);
|
||||
m_cut->Enable(island_picked);
|
||||
m_join->Enable(island_picked);
|
||||
m_unjoin->Enable(island_picked);
|
||||
m_snap->Enable(has_islands);
|
||||
const wxString no_island = !not_unwrapped.empty() ? not_unwrapped :
|
||||
island_picked ? wxString() :
|
||||
_L("Click an island on the canvas first, in Island mode.");
|
||||
m_avg_scale->SetUnavailable(not_unwrapped);
|
||||
m_cut->SetUnavailable(no_island);
|
||||
m_join->SetUnavailable(no_island);
|
||||
m_unjoin->SetUnavailable(no_island);
|
||||
m_snap->SetUnavailable(not_unwrapped);
|
||||
m_snap->SetValue(m_canvas->snap_enabled());
|
||||
m_frame->Enable(has_islands);
|
||||
m_frame->SetUnavailable(not_unwrapped);
|
||||
}
|
||||
|
||||
void UVEditorPanel::on_tool(wxCommandEvent &evt)
|
||||
|
||||
@@ -200,6 +200,7 @@ private:
|
||||
void on_paint(wxPaintEvent &evt);
|
||||
void on_size(wxSizeEvent &evt);
|
||||
void on_mouse(wxMouseEvent &evt);
|
||||
void on_capture_lost(wxMouseCaptureLostEvent &evt);
|
||||
void on_key(wxKeyEvent &evt);
|
||||
void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas
|
||||
void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op
|
||||
@@ -244,6 +245,16 @@ private:
|
||||
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
|
||||
Vec2f snap_correction(int island) const;
|
||||
void end_gesture();
|
||||
// Cancels the gesture instead of finishing it: nothing is committed, and a modal rotate/scale is
|
||||
// put back the way Esc puts it back. Used when the capture is taken away from us.
|
||||
void cancel_gesture();
|
||||
// One capture at a time, released exactly once. Pressing a second button mid-drag would otherwise
|
||||
// nest a second capture that the single release on button-up cannot undo, and macOS never sends
|
||||
// wxEVT_MOUSE_CAPTURE_LOST to recover from that. A leaked capture is not a local problem there:
|
||||
// while any wx window holds one, wxOSX routes every mouse event to it and the application stops
|
||||
// seeing enter/leave and motion entirely, which also takes its tooltips down.
|
||||
void grab_mouse();
|
||||
void drop_mouse();
|
||||
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
|
||||
void update_status();
|
||||
// Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed.
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/settings.h>
|
||||
#include <wx/tokenzr.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/variant.h>
|
||||
#include <wx/window.h>
|
||||
#include <wx/toplevel.h>
|
||||
@@ -62,7 +63,6 @@
|
||||
#include "PresetComboBoxes.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/DialogButtons.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
|
||||
#ifdef __linux__
|
||||
#define wxLinux true
|
||||
@@ -1011,8 +1011,14 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_
|
||||
checkbox_sizer->Show(bool(m_buttons & REMEMBER_CHOISE));
|
||||
|
||||
if (dependent_presets != nullptr) {
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/transfer_discard_changes");
|
||||
m_sizer_button->Add(wiki, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(22));
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/transfer_discard_changes";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
m_sizer_button->Add(wiki_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(22));
|
||||
}
|
||||
|
||||
m_sizer_button->Add(0, 0, 1, 0, 0);
|
||||
|
||||
@@ -223,6 +223,12 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
|
||||
this->SetMinSize(FromDIP(wxSize(26, 26)));
|
||||
this->SetSize(FromDIP(wxSize(26, 26)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
} else if (type == ButtonType::Circle) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(6, 6)));
|
||||
this->SetMinSize(FromDIP(wxSize(25, 25)));
|
||||
this->SetMaxSize(FromDIP(wxSize(25, 25)));
|
||||
this->SetSize(FromDIP(wxSize(25, 25)));
|
||||
this->SetCornerRadius(this->FromDIP(12));
|
||||
} else if (type == ButtonType::Expanded) {
|
||||
this->SetMinSize(FromDIP(wxSize(-1, 32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
||||
|
||||
@@ -34,6 +34,7 @@ enum class ButtonType {
|
||||
Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes
|
||||
Icon, // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while
|
||||
// creation of button
|
||||
Circle, // ------ FullyRounded Same as icon
|
||||
Expanded, // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cstddef>
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include <wx/colour.h>
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include "slic3r/GUI/Widgets/LabeledStaticBox.hpp"
|
||||
#include "slic3r/GUI/Widgets/RadioGroup.hpp"
|
||||
@@ -25,7 +26,6 @@
|
||||
#include <wx/dcgraph.h>
|
||||
#include "MainFrame.hpp"
|
||||
#include "Widgets/DialogButtons.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/valtext.h>
|
||||
#include <wx/tglbtn.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/event.h>
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Flow.hpp"
|
||||
@@ -237,8 +238,15 @@ PA_Calibration_Dlg::PA_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plater*
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/pressure_advance_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -459,8 +467,15 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/temp_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/temp_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -642,8 +657,15 @@ MaxVolumetricSpeed_Test_Dlg::MaxVolumetricSpeed_Test_Dlg(wxWindow* parent, wxWin
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/volumetric_speed_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/volumetric_speed_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -766,8 +788,15 @@ VFA_Test_Dlg::VFA_Test_Dlg(wxWindow* parent, wxWindowID id, Plater* plater)
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/vfa_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/vfa_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -1005,8 +1034,15 @@ Retraction_Test_Dlg::Retraction_Test_Dlg(wxWindow* parent, wxWindowID id, Plater
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/retraction_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/retraction_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -1185,8 +1221,15 @@ Input_Shaping_Freq_Test_Dlg::Input_Shaping_Freq_Test_Dlg(wxWindow* parent, wxWin
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/input_shaping_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/input_shaping_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -1384,8 +1427,15 @@ Input_Shaping_Damp_Test_Dlg::Input_Shaping_Damp_Test_Dlg(wxWindow* parent, wxWin
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/input_shaping_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/input_shaping_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -1580,8 +1630,15 @@ Cornering_Test_Dlg::Cornering_Test_Dlg(wxWindow* parent, wxWindowID id, Plater*
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/cornering_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/cornering_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
@@ -1719,8 +1776,15 @@ FlowRateCalibrationDialog::FlowRateCalibrationDialog(wxWindow* parent, wxWindowI
|
||||
auto dlg_btns = new DialogButtons(this, {"OK"});
|
||||
|
||||
auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/flow_ratio_calib");
|
||||
bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20));
|
||||
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/flow_ratio_calib";
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
bottom_sizer->AddStretchSpacer();
|
||||
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
|
||||
|
||||
@@ -947,13 +947,12 @@ std::string Http::tls_global_init()
|
||||
|
||||
std::string Http::tls_system_cert_store()
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
#ifdef OPENSSL_CERT_OVERRIDE
|
||||
ret = ::getenv(X509_get_default_cert_file_env());
|
||||
if (const char *cert_file = ::getenv(X509_get_default_cert_file_env()))
|
||||
return cert_file;
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string Http::url_encode(const std::string &str)
|
||||
|
||||
@@ -25,11 +25,19 @@ extern const char* const INSTALL_STATE_FILE;
|
||||
// Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible
|
||||
// values, so both go through these.
|
||||
|
||||
inline pybind11::object json_to_py(const nlohmann::json& j)
|
||||
// Maximum nesting depth for JSON <-> Python conversion. A self-referential or pathologically
|
||||
// deep value would otherwise recurse until the native C stack overflows, an uncatchable crash;
|
||||
// past this bound we raise instead. 200 is far beyond any legitimate plugin config or UI payload.
|
||||
inline constexpr int kMaxJsonConversionDepth = 200;
|
||||
|
||||
inline pybind11::object json_to_py(const nlohmann::json& j, int depth = 0)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
using json = nlohmann::json;
|
||||
|
||||
if (depth > kMaxJsonConversionDepth)
|
||||
throw py::value_error("Plugin JSON value nested too deeply");
|
||||
|
||||
switch (j.type()) {
|
||||
case json::value_t::null: return py::none();
|
||||
case json::value_t::boolean: return py::bool_(j.get<bool>());
|
||||
@@ -40,24 +48,27 @@ inline pybind11::object json_to_py(const nlohmann::json& j)
|
||||
case json::value_t::array: {
|
||||
py::list lst;
|
||||
for (const auto& e : j)
|
||||
lst.append(json_to_py(e));
|
||||
lst.append(json_to_py(e, depth + 1));
|
||||
return lst;
|
||||
}
|
||||
case json::value_t::object: {
|
||||
py::dict d;
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
d[py::str(it.key())] = json_to_py(it.value());
|
||||
d[py::str(it.key())] = json_to_py(it.value(), depth + 1);
|
||||
return d;
|
||||
}
|
||||
default: return py::none();
|
||||
}
|
||||
}
|
||||
|
||||
inline nlohmann::json py_to_json(const pybind11::handle& o)
|
||||
inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
using json = nlohmann::json;
|
||||
|
||||
if (depth > kMaxJsonConversionDepth)
|
||||
throw py::value_error("Plugin value nested too deeply (possible cycle)");
|
||||
|
||||
if (o.is_none())
|
||||
return json(nullptr);
|
||||
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
|
||||
@@ -73,13 +84,13 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
|
||||
if (py::isinstance<py::dict>(o)) {
|
||||
json obj = json::object();
|
||||
for (auto item : py::reinterpret_borrow<py::dict>(o))
|
||||
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
|
||||
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second, depth + 1);
|
||||
return obj;
|
||||
}
|
||||
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
|
||||
json arr = json::array();
|
||||
for (auto e : o)
|
||||
arr.push_back(py_to_json(e));
|
||||
arr.push_back(py_to_json(e, depth + 1));
|
||||
return arr;
|
||||
}
|
||||
return py::str(o).cast<std::string>(); // fallback: str()
|
||||
|
||||
@@ -11,6 +11,13 @@ endif ()
|
||||
|
||||
add_test(NAME cli_strict_mode
|
||||
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
|
||||
add_test(NAME cli_empty_project_config
|
||||
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_empty_project_config.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON} ${CMAKE_SOURCE_DIR})
|
||||
set_tests_properties(cli_empty_project_config PROPERTIES
|
||||
LABELS "CLI;RequiresApp"
|
||||
SKIP_RETURN_CODE 77
|
||||
TIMEOUT 300)
|
||||
|
||||
set_tests_properties(cli_strict_mode PROPERTIES
|
||||
LABELS "CLI;RequiresApp"
|
||||
SKIP_RETURN_CODE 77
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression check: a 3mf whose Metadata/project_settings.config carries no settings must load.
|
||||
#
|
||||
# The CLI reads printable_height out of the project config with opt_float(), which dereferences
|
||||
# what option<>() returns. With create = false that is null when the key is absent, so a project
|
||||
# saved without settings used to take the CLI down with a segfault. Both models in
|
||||
# resources/handy_models are such files.
|
||||
#
|
||||
# usage: test_cli_empty_project_config.sh <orca-slicer binary> <python3> <source dir>
|
||||
set -u
|
||||
|
||||
BIN="${1:-}"
|
||||
PY="${2:-python3}"
|
||||
SRC="${3:-}"
|
||||
# 77 is the test's SKIP_RETURN_CODE.
|
||||
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
|
||||
[ -f "$SRC/resources/handy_models/OrcaBadge.3mf" ] || { echo "SKIP: handy model not found"; exit 77; }
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-emptycfg.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
mkdir -p "$WORK/datadir"
|
||||
|
||||
# Rewrite the project settings to an empty object, so the test holds no matter what the shipped
|
||||
# models carry later on.
|
||||
cp "$SRC/resources/handy_models/OrcaBadge.3mf" "$WORK/empty_config.3mf"
|
||||
"$PY" - "$WORK/empty_config.3mf" <<'PYEOF'
|
||||
import shutil, sys, zipfile
|
||||
|
||||
path = sys.argv[1]
|
||||
entry = "Metadata/project_settings.config"
|
||||
with zipfile.ZipFile(path) as src:
|
||||
items = [(i, src.read(i.filename)) for i in src.infolist()]
|
||||
with zipfile.ZipFile(path + ".new", "w", zipfile.ZIP_DEFLATED) as dst:
|
||||
seen = False
|
||||
for info, data in items:
|
||||
if info.filename == entry:
|
||||
data, seen = b"{\n}\n", True
|
||||
dst.writestr(info, data)
|
||||
if not seen:
|
||||
dst.writestr(entry, b"{\n}\n")
|
||||
shutil.move(path + ".new", path)
|
||||
PYEOF
|
||||
|
||||
"$BIN" --datadir "$WORK/datadir" --info "$WORK/empty_config.3mf" > "$WORK/info.txt" 2>&1
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FAIL: --info on a project with empty settings exited $rc"
|
||||
tail -20 "$WORK/info.txt"
|
||||
exit 1
|
||||
fi
|
||||
grep -q "size_x" "$WORK/info.txt" || { echo "FAIL: --info printed no geometry"; cat "$WORK/info.txt"; exit 1; }
|
||||
echo "PASS: a project with empty settings loads"
|
||||
@@ -0,0 +1,166 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
|
||||
FILE_NAME('Open CASCADE Shape Model','2026-10-08T09:57:33',('Author'),(
|
||||
'Open CASCADE'),'Open CASCADE STEP processor 8.0','Open CASCADE 8.0'
|
||||
,'Unknown');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
|
||||
'automotive_design',2000,#2);
|
||||
#2 = APPLICATION_CONTEXT(
|
||||
'core data for automotive mechanical design processes');
|
||||
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
|
||||
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
|
||||
#5 = PRODUCT_DEFINITION('design','',#6,#9);
|
||||
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
|
||||
#7 = PRODUCT('Open CASCADE STEP translator 8.0 1',
|
||||
'Open CASCADE STEP translator 8.0 1','',(#8));
|
||||
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
|
||||
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
|
||||
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#121);
|
||||
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
|
||||
#12 = CARTESIAN_POINT('',(0.,0.,0.));
|
||||
#13 = DIRECTION('',(0.,0.,1.));
|
||||
#14 = DIRECTION('',(1.,0.,-0.));
|
||||
#15 = MANIFOLD_SOLID_BREP('',#16);
|
||||
#16 = CLOSED_SHELL('',(#17,#113,#117));
|
||||
#17 = ADVANCED_FACE('',(#18),#31,.T.);
|
||||
#18 = FACE_BOUND('',#19,.T.);
|
||||
#19 = EDGE_LOOP('',(#20,#58,#81,#112));
|
||||
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
|
||||
#21 = EDGE_CURVE('',#22,#22,#24,.T.);
|
||||
#22 = VERTEX_POINT('',#23);
|
||||
#23 = CARTESIAN_POINT('',(-36.4,0.,25.));
|
||||
#24 = SURFACE_CURVE('',#25,(#30,#42),.PCURVE_S1.);
|
||||
#25 = CIRCLE('',#26,36.4);
|
||||
#26 = AXIS2_PLACEMENT_3D('',#27,#28,#29);
|
||||
#27 = CARTESIAN_POINT('',(0.,0.,25.));
|
||||
#28 = DIRECTION('',(0.,0.,1.));
|
||||
#29 = DIRECTION('',(1.,0.,-0.));
|
||||
#30 = PCURVE('',#31,#36);
|
||||
#31 = CONICAL_SURFACE('',#32,36.4,0.785398163397);
|
||||
#32 = AXIS2_PLACEMENT_3D('',#33,#34,#35);
|
||||
#33 = CARTESIAN_POINT('',(0.,0.,25.));
|
||||
#34 = DIRECTION('',(0.,0.,1.));
|
||||
#35 = DIRECTION('',(1.,0.,-0.));
|
||||
#36 = DEFINITIONAL_REPRESENTATION('',(#37),#41);
|
||||
#37 = LINE('',#38,#39);
|
||||
#38 = CARTESIAN_POINT('',(-6.28318530718,3.552713678801E-15));
|
||||
#39 = VECTOR('',#40,1.);
|
||||
#40 = DIRECTION('',(1.,0.));
|
||||
#41 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
||||
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
||||
) );
|
||||
#42 = PCURVE('',#43,#48);
|
||||
#43 = PLANE('',#44);
|
||||
#44 = AXIS2_PLACEMENT_3D('',#45,#46,#47);
|
||||
#45 = CARTESIAN_POINT('',(0.,0.,25.));
|
||||
#46 = DIRECTION('',(-0.,-0.,-1.));
|
||||
#47 = DIRECTION('',(-1.,0.,0.));
|
||||
#48 = DEFINITIONAL_REPRESENTATION('',(#49),#57);
|
||||
#49 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#50,#51,#52,#53,#54,#55,#56),
|
||||
.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
|
||||
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
|
||||
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
|
||||
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
|
||||
'') );
|
||||
#50 = CARTESIAN_POINT('',(-36.4,0.));
|
||||
#51 = CARTESIAN_POINT('',(-36.4,63.046649395507));
|
||||
#52 = CARTESIAN_POINT('',(18.2,31.523324697754));
|
||||
#53 = CARTESIAN_POINT('',(72.8,8.915428697793E-15));
|
||||
#54 = CARTESIAN_POINT('',(18.2,-31.52332469775));
|
||||
#55 = CARTESIAN_POINT('',(-36.4,-63.0466493955));
|
||||
#56 = CARTESIAN_POINT('',(-36.4,0.));
|
||||
#57 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
||||
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
||||
) );
|
||||
#58 = ORIENTED_EDGE('',*,*,#59,.T.);
|
||||
#59 = EDGE_CURVE('',#22,#60,#62,.T.);
|
||||
#60 = VERTEX_POINT('',#61);
|
||||
#61 = CARTESIAN_POINT('',(-11.4,0.,3.552713678801E-15));
|
||||
#62 = SEAM_CURVE('',#63,(#67,#74),.PCURVE_S1.);
|
||||
#63 = LINE('',#64,#65);
|
||||
#64 = CARTESIAN_POINT('',(-36.4,0.,25.));
|
||||
#65 = VECTOR('',#66,1.);
|
||||
#66 = DIRECTION('',(0.707106781187,8.659560562349E-17,-0.707106781187));
|
||||
#67 = PCURVE('',#31,#68);
|
||||
#68 = DEFINITIONAL_REPRESENTATION('',(#69),#73);
|
||||
#69 = LINE('',#70,#71);
|
||||
#70 = CARTESIAN_POINT('',(-3.14159265359,3.552713678801E-15));
|
||||
#71 = VECTOR('',#72,1.);
|
||||
#72 = DIRECTION('',(2.13162820728E-16,-1.));
|
||||
#73 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
||||
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
||||
) );
|
||||
#74 = PCURVE('',#31,#75);
|
||||
#75 = DEFINITIONAL_REPRESENTATION('',(#76),#80);
|
||||
#76 = LINE('',#77,#78);
|
||||
#77 = CARTESIAN_POINT('',(3.14159265359,3.552713678801E-15));
|
||||
#78 = VECTOR('',#79,1.);
|
||||
#79 = DIRECTION('',(2.13162820728E-16,-1.));
|
||||
#80 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
||||
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
||||
) );
|
||||
#81 = ORIENTED_EDGE('',*,*,#82,.F.);
|
||||
#82 = EDGE_CURVE('',#60,#60,#83,.T.);
|
||||
#83 = SURFACE_CURVE('',#84,(#89,#96),.PCURVE_S1.);
|
||||
#84 = CIRCLE('',#85,11.4);
|
||||
#85 = AXIS2_PLACEMENT_3D('',#86,#87,#88);
|
||||
#86 = CARTESIAN_POINT('',(0.,0.,3.552713678801E-15));
|
||||
#87 = DIRECTION('',(0.,0.,-1.));
|
||||
#88 = DIRECTION('',(1.,0.,0.));
|
||||
#89 = PCURVE('',#31,#90);
|
||||
#90 = DEFINITIONAL_REPRESENTATION('',(#91),#95);
|
||||
#91 = LINE('',#92,#93);
|
||||
#92 = CARTESIAN_POINT('',(6.28318530718,-25.));
|
||||
#93 = VECTOR('',#94,1.);
|
||||
#94 = DIRECTION('',(-1.,0.));
|
||||
#95 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
||||
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
||||
) );
|
||||
#96 = PCURVE('',#97,#102);
|
||||
#97 = PLANE('',#98);
|
||||
#98 = AXIS2_PLACEMENT_3D('',#99,#100,#101);
|
||||
#99 = CARTESIAN_POINT('',(0.,0.,3.552713678801E-15));
|
||||
#100 = DIRECTION('',(0.,0.,1.));
|
||||
#101 = DIRECTION('',(1.,0.,-0.));
|
||||
#102 = DEFINITIONAL_REPRESENTATION('',(#103),#111);
|
||||
#103 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#104,#105,#106,#107,#108,#109
|
||||
,#110),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
|
||||
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
|
||||
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
|
||||
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
|
||||
'') );
|
||||
#104 = CARTESIAN_POINT('',(11.4,0.));
|
||||
#105 = CARTESIAN_POINT('',(11.4,-19.74537920628));
|
||||
#106 = CARTESIAN_POINT('',(-5.7,-9.872689603143));
|
||||
#107 = CARTESIAN_POINT('',(-22.8,-2.792194702056E-15));
|
||||
#108 = CARTESIAN_POINT('',(-5.7,9.872689603143));
|
||||
#109 = CARTESIAN_POINT('',(11.4,19.745379206285));
|
||||
#110 = CARTESIAN_POINT('',(11.4,0.));
|
||||
#111 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
||||
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
||||
) );
|
||||
#112 = ORIENTED_EDGE('',*,*,#59,.F.);
|
||||
#113 = ADVANCED_FACE('',(#114),#43,.F.);
|
||||
#114 = FACE_BOUND('',#115,.F.);
|
||||
#115 = EDGE_LOOP('',(#116));
|
||||
#116 = ORIENTED_EDGE('',*,*,#21,.F.);
|
||||
#117 = ADVANCED_FACE('',(#118),#97,.F.);
|
||||
#118 = FACE_BOUND('',#119,.F.);
|
||||
#119 = EDGE_LOOP('',(#120));
|
||||
#120 = ORIENTED_EDGE('',*,*,#82,.F.);
|
||||
#121 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
||||
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#125)) GLOBAL_UNIT_ASSIGNED_CONTEXT
|
||||
((#122,#123,#124)) REPRESENTATION_CONTEXT('Context #1',
|
||||
'3D Context with UNIT and UNCERTAINTY') );
|
||||
#122 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#123 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#124 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#125 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#122,
|
||||
'distance_accuracy_value','confusion accuracy');
|
||||
#126 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1780,7 +1780,7 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal
|
||||
const AABBTreeLines::LinesDistancer<Line> printed_tree(to_lines(printed));
|
||||
|
||||
// Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently.
|
||||
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr),
|
||||
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr),
|
||||
shrink(to_polygons(layer.lslices), scale_(3.)));
|
||||
REQUIRE_FALSE(anchors.empty());
|
||||
double max_distance = 0.;
|
||||
@@ -1792,8 +1792,9 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal
|
||||
CHECK(unscale<double>(max_distance) <= config.opt_float("resolution"));
|
||||
}
|
||||
|
||||
// Orca: Slices the meshes as the parts of one object, where they are.
|
||||
static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector<TriangleMesh> &parts)
|
||||
// Orca: Slices the meshes as the parts of one object, where they are, with modifiers of their own config.
|
||||
static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector<TriangleMesh> &parts,
|
||||
const std::vector<std::pair<TriangleMesh, DynamicPrintConfig>> &modifiers = {})
|
||||
{
|
||||
config.set_deserialize_strict({{"layer_height", 0.2},
|
||||
{"initial_layer_print_height", 0.2},
|
||||
@@ -1804,6 +1805,8 @@ static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::ve
|
||||
Slic3r::Test::init_print({parts.front()}, print, model, config, nullptr, false);
|
||||
for (size_t i = 1; i < parts.size(); ++ i)
|
||||
model.objects.front()->add_volume(TriangleMesh(parts[i]), ModelVolumeType::MODEL_PART, false);
|
||||
for (const auto &[mesh, modifier_config] : modifiers)
|
||||
model.objects.front()->add_volume(TriangleMesh(mesh), ModelVolumeType::PARAMETER_MODIFIER, false)->config.apply(modifier_config);
|
||||
print.apply(model, config);
|
||||
print.process();
|
||||
return print;
|
||||
@@ -1988,3 +1991,33 @@ TEST_CASE("Adaptive infill fills each body like the body sliced alone", "[Fill][
|
||||
CHECK(unmatched.first < 0.02);
|
||||
CHECK(unmatched.second < 0.02);
|
||||
}
|
||||
|
||||
TEST_CASE("Adaptive infill of a modifier leaves the density of the other regions", "[Fill][Regression]")
|
||||
{
|
||||
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
|
||||
CAPTURE(pattern);
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
|
||||
{"sparse_infill_density", "15%"},
|
||||
{"top_shell_layers", 0},
|
||||
{"bottom_shell_layers", 0}});
|
||||
TriangleMesh bodies = make_cube(30, 30, 6), second = make_cube(30, 30, 6);
|
||||
second.translate(40, 0, 0);
|
||||
bodies.merge(second);
|
||||
// Orca: A denser modifier over the right half of the second body.
|
||||
TriangleMesh modifier = make_cube(20, 40, 10);
|
||||
modifier.translate(55, -5, -2);
|
||||
DynamicPrintConfig dense = config;
|
||||
dense.set_deserialize_strict({{"sparse_infill_density", "60%"}});
|
||||
Print print, print_sparse, print_dense;
|
||||
slice_parts(print, config, {bodies}, {{modifier, dense}});
|
||||
slice_parts(print_sparse, config, {bodies});
|
||||
slice_parts(print_dense, dense, {bodies});
|
||||
|
||||
// Orca: Bed regions 3 mm inside the walls and the modifier, away from the links along them.
|
||||
auto rect = [](double x0, double y0, double x1, double y1) {
|
||||
return Polygon({Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)});
|
||||
};
|
||||
CHECK(unmatched_between_prints(print, print_sparse, erInternalInfill, {rect(3, 3, 27, 27), rect(43, 3, 52, 27)}) < 0.02);
|
||||
CHECK(unmatched_between_prints(print, print_dense, erInternalInfill, {rect(58, 3, 67, 27)}) < 0.02);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
#include "test_helpers.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <sstream>
|
||||
@@ -221,3 +226,94 @@ TEST_CASE("Line ends of the exported G-code mark every newline in the file", "[G
|
||||
INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1);
|
||||
CHECK(difference.first == result.lines_ends.end());
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Writes the bytes verbatim: the G-code export is binary, so no newline translation must creep in
|
||||
// here either, otherwise the CRLF cases below would silently test LF.
|
||||
void write_gcode(const std::string &path, const std::string &content)
|
||||
{
|
||||
boost::nowide::ofstream f(path, std::ios::binary);
|
||||
f << content;
|
||||
}
|
||||
|
||||
// Cuts line `id` (1-based) out of the file the way GCodeViewer's G-code window does: the text it
|
||||
// shows is bytes [lines_ends[id - 2], lines_ends[id - 1]). Reproducing that here is what makes these
|
||||
// assertions about the rendered result rather than about the offsets themselves.
|
||||
std::string line_at(const std::string &path, const std::vector<size_t> &lines_ends, size_t id)
|
||||
{
|
||||
boost::nowide::ifstream f(path, std::ios::binary);
|
||||
const std::string bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
const size_t start = id == 1 ? 0 : lines_ends[id - 2];
|
||||
return bytes.substr(start, lines_ends[id - 1] - start);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("rebuild_lines_ends realigns the line offsets after the G-code is rewritten in place", "[GCodeProcessor]")
|
||||
{
|
||||
ScopedTemporaryFile tmp(".gcode");
|
||||
GCodeProcessorResult result;
|
||||
result.filename = tmp.string();
|
||||
|
||||
write_gcode(tmp.string(), "G1 X1\nG1 X2\nG1 X3\n");
|
||||
REQUIRE(result.rebuild_lines_ends());
|
||||
REQUIRE(result.lines_ends.size() == 3);
|
||||
CHECK(line_at(tmp.string(), result.lines_ends, 2) == "G1 X2\n");
|
||||
|
||||
// What a post-processing script that opens the file in Python text mode on Windows does: prepend a
|
||||
// comment and translate every LF to CRLF. Every byte offset shifts, and cumulatively.
|
||||
write_gcode(tmp.string(), ";EDITED\r\nG1 X1\r\nG1 X2\r\nG1 X3\r\n");
|
||||
CHECK(line_at(tmp.string(), result.lines_ends, 2) != "G1 X1\r\n");
|
||||
|
||||
REQUIRE(result.rebuild_lines_ends());
|
||||
REQUIRE(result.lines_ends.size() == 4);
|
||||
CHECK(line_at(tmp.string(), result.lines_ends, 1) == ";EDITED\r\n");
|
||||
CHECK(line_at(tmp.string(), result.lines_ends, 2) == "G1 X1\r\n");
|
||||
CHECK(line_at(tmp.string(), result.lines_ends, 4) == "G1 X3\r\n");
|
||||
}
|
||||
|
||||
TEST_CASE("rebuild_lines_ends maps a file longer than one read chunk", "[GCodeProcessor]")
|
||||
{
|
||||
ScopedTemporaryFile tmp(".gcode");
|
||||
GCodeProcessorResult result;
|
||||
result.filename = tmp.string();
|
||||
|
||||
const size_t line_count = 20000;
|
||||
std::string content;
|
||||
for (size_t i = 0; i < line_count; ++i)
|
||||
content += "G1 X" + std::to_string(i) + "\n";
|
||||
// The scan reads the file in chunks, so it has to span at least one chunk boundary to be meaningful.
|
||||
REQUIRE(content.size() > 65536);
|
||||
write_gcode(tmp.string(), content);
|
||||
|
||||
REQUIRE(result.rebuild_lines_ends());
|
||||
REQUIRE(result.lines_ends.size() == line_count);
|
||||
CHECK(result.lines_ends.back() == content.size());
|
||||
CHECK(line_at(tmp.string(), result.lines_ends, line_count) == "G1 X" + std::to_string(line_count - 1) + "\n");
|
||||
}
|
||||
|
||||
TEST_CASE("rebuild_lines_ends ignores a trailing line that has no newline", "[GCodeProcessor]")
|
||||
{
|
||||
ScopedTemporaryFile tmp(".gcode");
|
||||
GCodeProcessorResult result;
|
||||
result.filename = tmp.string();
|
||||
|
||||
// Matches the export pass, which only ever records the offset one past a '\n'.
|
||||
write_gcode(tmp.string(), "G1 X1\nG1 X2");
|
||||
REQUIRE(result.rebuild_lines_ends());
|
||||
CHECK(result.lines_ends.size() == 1);
|
||||
CHECK(line_at(tmp.string(), result.lines_ends, 1) == "G1 X1\n");
|
||||
}
|
||||
|
||||
TEST_CASE("rebuild_lines_ends empties the map when the G-code file cannot be read", "[GCodeProcessor]")
|
||||
{
|
||||
// A partially rebuilt map would have the G-code window slicing lines at wrong offsets again; an
|
||||
// empty one just hides the window.
|
||||
GCodeProcessorResult result;
|
||||
result.filename = (boost::filesystem::temp_directory_path() / "orca-nonexistent-gcode-file.gcode").string();
|
||||
result.lines_ends = {1, 2, 3};
|
||||
|
||||
CHECK_FALSE(result.rebuild_lines_ends());
|
||||
CHECK(result.lines_ends.empty());
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_geometry.cpp
|
||||
test_multimaterial_segmentation.cpp
|
||||
test_placeholder_parser.cpp
|
||||
test_png_read_write.cpp
|
||||
test_polygon.cpp
|
||||
test_mutable_polygon.cpp
|
||||
test_mutable_priority_queue.cpp
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <libslic3r/MeshBoolean.hpp>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
@@ -24,3 +26,39 @@ TEST_CASE("CGAL and TriangleMesh conversions", "[MeshBoolean]") {
|
||||
|
||||
REQUIRE(! MeshBoolean::cgal::does_self_intersect(M));
|
||||
}
|
||||
|
||||
TEST_CASE("mcut difference handles source splits between cuts", "[MeshBoolean]") {
|
||||
TriangleMesh body = make_cube(30., 10., 10.);
|
||||
|
||||
TriangleMesh tool;
|
||||
|
||||
// First cut splits the source into two disconnected components.
|
||||
TriangleMesh slab = make_cube(2., 12., 20.);
|
||||
slab.translate(Vec3f(14.f, -1.f, -5.f));
|
||||
its_merge(tool.its, slab.its);
|
||||
|
||||
// These cuts must still be applied after the source has been split.
|
||||
TriangleMesh left_hole = make_cube(4., 4., 20.);
|
||||
left_hole.translate(Vec3f(3.f, 3.f, -5.f));
|
||||
its_merge(tool.its, left_hole.its);
|
||||
|
||||
TriangleMesh right_hole = make_cube(4., 4., 20.);
|
||||
right_hole.translate(Vec3f(21.f, 3.f, -5.f));
|
||||
its_merge(tool.its, right_hole.its);
|
||||
|
||||
std::vector<TriangleMesh> result;
|
||||
MeshBoolean::mcut::make_boolean(body, tool, result, "A_NOT_B");
|
||||
|
||||
REQUIRE(result.size() == 1);
|
||||
|
||||
const std::vector<indexed_triangle_set> components =
|
||||
its_split(result.front().its);
|
||||
|
||||
REQUIRE(components.size() == 2);
|
||||
|
||||
// 3000 - 200 - 160 - 160 = 2480.
|
||||
REQUIRE_THAT(
|
||||
result.front().volume(),
|
||||
Catch::Matchers::WithinRel(2480., 1e-3)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
// libpng reports a corrupt or truncated file by longjmp()ing out of the decoder, so the decoders have
|
||||
// to come back with false rather than crash or hand back a half filled image.
|
||||
namespace {
|
||||
|
||||
// A real PNG, produced by the writer next door, so the bytes are a file libpng accepts.
|
||||
std::vector<uint8_t> encoded_png(size_t w, size_t h)
|
||||
{
|
||||
std::vector<uint8_t> pixels(w * h);
|
||||
for (size_t i = 0; i < pixels.size(); ++ i)
|
||||
pixels[i] = uint8_t((i * 7) % 256);
|
||||
|
||||
const boost::filesystem::path path = boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path("png_rw_%%%%%%%%.png");
|
||||
REQUIRE(png::write_gray_to_file(path.string(), w, h, pixels));
|
||||
std::vector<uint8_t> bytes;
|
||||
{
|
||||
std::ifstream ifs(path.string(), std::ios::binary);
|
||||
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(path, ec);
|
||||
REQUIRE(bytes.size() > 64);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
png::ReadBuf buf_of(const std::vector<uint8_t> &bytes, size_t size)
|
||||
{
|
||||
return png::ReadBuf{ bytes.data(), size };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A whole PNG decodes", "[PNG]") {
|
||||
const std::vector<uint8_t> bytes = encoded_png(24, 16);
|
||||
|
||||
png::ImageGreyscale grey;
|
||||
REQUIRE(png::decode_png(buf_of(bytes, bytes.size()), grey));
|
||||
CHECK(grey.cols == 24);
|
||||
CHECK(grey.rows == 16);
|
||||
CHECK(grey.buf.size() == 24 * 16);
|
||||
}
|
||||
|
||||
TEST_CASE("A truncated PNG is refused instead of crashing", "[PNG]") {
|
||||
const std::vector<uint8_t> bytes = encoded_png(64, 64);
|
||||
|
||||
// Cut past the signature: inside the header, and inside the pixel data. Not in the trailing
|
||||
// chunks - decode_png() does not read those, so a file missing only its IEND still decodes, and
|
||||
// that is the pre-existing contract rather than anything this change touches.
|
||||
const size_t size = GENERATE_COPY(size_t(16), size_t(40), bytes.size() / 2, bytes.size() * 3 / 4);
|
||||
REQUIRE(size < bytes.size());
|
||||
|
||||
png::ImageGreyscale grey;
|
||||
CHECK_FALSE(png::decode_png(buf_of(bytes, size), grey));
|
||||
|
||||
png::ImageColorscale colour;
|
||||
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, size), colour));
|
||||
}
|
||||
|
||||
TEST_CASE("A PNG whose body is garbage is refused", "[PNG]") {
|
||||
std::vector<uint8_t> bytes = encoded_png(32, 32);
|
||||
// Keep the signature, scribble over everything after it.
|
||||
for (size_t i = 8; i < bytes.size(); ++ i)
|
||||
bytes[i] = uint8_t(0xA5);
|
||||
|
||||
png::ImageGreyscale grey;
|
||||
CHECK_FALSE(png::decode_png(buf_of(bytes, bytes.size()), grey));
|
||||
|
||||
png::ImageColorscale colour;
|
||||
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, bytes.size()), colour));
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Format/STEP.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -73,6 +74,23 @@ TEST_CASE("A security classification assignment does not crash import", "[Step]"
|
||||
CHECK(model.objects.front()->volumes.front()->mesh().facets_count() == 4); // a tetrahedron
|
||||
}
|
||||
|
||||
// The fixture is a truncated cone whose seam pcurves have the direction (2.1e-16, -1).
|
||||
TEST_CASE("A cone with a slightly tilted seam imports as a closed mesh", "[Step]")
|
||||
{
|
||||
const std::string path = std::string(TEST_DATA_DIR) + PATH_SEPARATOR "cone_tilted_seam_pcurve.step";
|
||||
|
||||
Model model;
|
||||
bool cancel = false;
|
||||
Step step(path);
|
||||
|
||||
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
|
||||
REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS);
|
||||
|
||||
REQUIRE(model.objects.size() == 1);
|
||||
REQUIRE(model.objects.front()->volumes.size() == 1);
|
||||
CHECK(its_num_open_edges(model.objects.front()->volumes.front()->mesh().its) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]")
|
||||
{
|
||||
CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9
|
||||
|
||||
@@ -34,6 +34,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_plugin_sort.cpp
|
||||
test_plugin_cloud_metadata.cpp
|
||||
test_plugin_audit.cpp
|
||||
test_plugin_json_depth.cpp
|
||||
test_shortcuts.cpp
|
||||
test_file_url.cpp
|
||||
test_user_manager.cpp
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||
|
||||
#include "plugin_test_utils.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/gil.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
// Brings the embedded interpreter up for one test and tears it down before boost::log does,
|
||||
// mirroring the ScopedPluginManager idiom in the other plugin tests.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
ScopedDataDir python_data_dir{"plugin-json-depth"};
|
||||
bool initialized = PluginManager::instance().initialize();
|
||||
~ScopedPluginManager()
|
||||
{
|
||||
PluginManager::instance().shutdown();
|
||||
PythonInterpreter::instance().shutdown();
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("py_to_json raises instead of overflowing on pathologically deep input", "[PluginHost][Python]")
|
||||
{
|
||||
ScopedPluginManager manager;
|
||||
REQUIRE(manager.initialized);
|
||||
namespace py = pybind11;
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
// [[[ ... 0 ... ]]] nested 300 deep: past the 200 conversion-depth cap, but shallow enough
|
||||
// that the pre-fix code returns without crashing, so a regression fails cleanly rather than
|
||||
// taking the process down. Built in C++ so the test does not depend on Python builtins.
|
||||
py::object deep = py::int_(0);
|
||||
for (int i = 0; i < 300; ++i) {
|
||||
py::list wrapper;
|
||||
wrapper.append(deep);
|
||||
deep = std::move(wrapper);
|
||||
}
|
||||
CHECK_THROWS_AS(py_to_json(deep), std::exception);
|
||||
}
|
||||
|
||||
TEST_CASE("py_to_json still converts reasonably nested input", "[PluginHost][Python]")
|
||||
{
|
||||
ScopedPluginManager manager;
|
||||
REQUIRE(manager.initialized);
|
||||
namespace py = pybind11;
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
py::dict d;
|
||||
d["a"] = py::int_(1);
|
||||
py::list inner;
|
||||
inner.append(py::str("x"));
|
||||
inner.append(py::int_(2));
|
||||
d["b"] = inner;
|
||||
|
||||
const nlohmann::json j = py_to_json(d);
|
||||
CHECK(j.at("a").get<std::int64_t>() == 1);
|
||||
CHECK(j.at("b").at(0).get<std::string>() == "x");
|
||||
CHECK(j.at("b").at(1).get<std::int64_t>() == 2);
|
||||
}
|
||||
Reference in New Issue
Block a user