mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-23 10:52:15 +00:00
POC
This commit is contained in:
@@ -29,6 +29,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_optimizers.cpp
|
||||
# test_png_io.cpp
|
||||
test_indexed_triangle_set.cpp
|
||||
test_texture_displacement.cpp
|
||||
../libnest2d/printer_parts.cpp
|
||||
)
|
||||
|
||||
|
||||
136
tests/libslic3r/test_texture_displacement.cpp
Normal file
136
tests/libslic3r/test_texture_displacement.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
#define NOMINMAX
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
// Encodes a flat (uniform-value) grayscale image through Slic3r's own PNG writer/reader round
|
||||
// trip, so decode_height_texture() (which only accepts true 8-bit grayscale PNG) is guaranteed a
|
||||
// compatible file, exactly like the GUI's "Add texture" import path does.
|
||||
static std::shared_ptr<std::vector<unsigned char>> make_flat_gray_png(uint8_t value, size_t w = 4, size_t h = 4)
|
||||
{
|
||||
std::vector<uint8_t> pixels(w * h, value);
|
||||
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
|
||||
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
|
||||
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
|
||||
|
||||
std::vector<unsigned char> bytes;
|
||||
{
|
||||
std::ifstream ifs(tmp_path.string(), std::ios::binary);
|
||||
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(tmp_path, ec);
|
||||
|
||||
REQUIRE_FALSE(bytes.empty());
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: decode_height_texture round-trips an 8-bit grayscale PNG", "[TextureDisplacement]")
|
||||
{
|
||||
TextureDisplacementLayer layer;
|
||||
layer.image_data = make_flat_gray_png(128, 4, 4);
|
||||
|
||||
DecodedHeightTexture tex = decode_height_texture(layer);
|
||||
REQUIRE_FALSE(tex.empty());
|
||||
CHECK(tex.width == 4);
|
||||
CHECK(tex.height == 4);
|
||||
REQUIRE_THAT(tex.sample(Vec2f(0.5f, 0.5f)), WithinAbs(128.0 / 255.0, 1.0 / 255.0));
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: an empty layer list leaves the mesh unchanged", "[TextureDisplacement]")
|
||||
{
|
||||
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
|
||||
const std::vector<TextureDisplacementLayer> layers; // none
|
||||
TextureDisplacementFacetsData facets{}; // all empty
|
||||
|
||||
const indexed_triangle_set result = build_texture_displacement(cube, layers, facets);
|
||||
|
||||
REQUIRE(result.vertices.size() == cube.vertices.size());
|
||||
REQUIRE(result.indices.size() == cube.indices.size());
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
CHECK(result.vertices[i](c) == cube.vertices[i](c));
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: fully painting a mesh displaces every vertex along its own normal", "[TextureDisplacement]")
|
||||
{
|
||||
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
|
||||
const TriangleMesh cube_mesh(cube);
|
||||
|
||||
TriangleSelector selector(cube_mesh);
|
||||
for (int f = 0; f < int(cube.indices.size()); ++f)
|
||||
selector.set_facet(f, EnforcerBlockerType::ENFORCER);
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = selector.serialize();
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 2.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_gray_png(255); // sample() == 1.0 everywhere -> full depth_mm displacement
|
||||
|
||||
const indexed_triangle_set result = build_texture_displacement(cube, {layer}, facets);
|
||||
|
||||
REQUIRE(result.vertices.size() == cube.vertices.size());
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i) {
|
||||
const float moved = (result.vertices[i] - cube.vertices[i]).norm();
|
||||
CHECK_THAT(moved, WithinAbs(layer.depth_mm, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: boundary vertices shared with unpainted triangles are pinned", "[TextureDisplacement]")
|
||||
{
|
||||
// A small triangle fan around a central vertex O, with 4 outer points A/B/C/D forming 4
|
||||
// triangles T0..T3 in the XY plane. Only T0, T1, T2 are painted, T3 is left unpainted:
|
||||
// O: touches all 4 triangles (incl. unpainted T3) -> boundary, must NOT move
|
||||
// A: touches T0 (painted) and T3 (unpainted) -> boundary, must NOT move
|
||||
// D: touches T2 (painted) and T3 (unpainted) -> boundary, must NOT move
|
||||
// B: touches only T0 and T1 (both painted) -> interior, SHOULD move
|
||||
// C: touches only T1 and T2 (both painted) -> interior, SHOULD move
|
||||
indexed_triangle_set fan;
|
||||
fan.vertices = { {0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {-1.f, 0.f, 0.f}, {0.f, -1.f, 0.f} };
|
||||
fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} };
|
||||
|
||||
const TriangleMesh fan_mesh(fan);
|
||||
TriangleSelector selector(fan_mesh);
|
||||
selector.set_facet(0, EnforcerBlockerType::ENFORCER);
|
||||
selector.set_facet(1, EnforcerBlockerType::ENFORCER);
|
||||
selector.set_facet(2, EnforcerBlockerType::ENFORCER);
|
||||
// facet 3 (T3) is left at its default EnforcerBlockerType::NONE.
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = selector.serialize();
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 1.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_gray_png(255);
|
||||
|
||||
const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets);
|
||||
|
||||
// Find each named vertex's post-bake position by matching the original (pinned vertices keep
|
||||
// their exact original position; moved ones won't match any original position anymore).
|
||||
auto still_at_original_position = [&](const Vec3f &original) {
|
||||
for (const Vec3f &v : result.vertices)
|
||||
if ((v - original).norm() < 1e-6f)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
CHECK(still_at_original_position(fan.vertices[0])); // O: boundary
|
||||
CHECK(still_at_original_position(fan.vertices[1])); // A: boundary
|
||||
CHECK(still_at_original_position(fan.vertices[4])); // D: boundary
|
||||
CHECK_FALSE(still_at_original_position(fan.vertices[2])); // B: interior, must have moved
|
||||
CHECK_FALSE(still_at_original_position(fan.vertices[3])); // C: interior, must have moved
|
||||
}
|
||||
Reference in New Issue
Block a user