mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 23:31:19 +00:00
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes. clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer. * Remove Unused Project Includes From Files With Platform-Specific Code A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block. * Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass The platform-file pass treated pchheader.hpp as an ordinary header and emptied it, and left GUI_Preview.hpp and 14 other files relying on headers they no longer reached directly. * Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call * Include Headers That Files Reached Through Ones the Cleanup Removed * Drop Includes Duplicated by the Cleanup or by Main's Own Additions * Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
437 lines
20 KiB
C++
437 lines
20 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include <cstddef>
|
|
#include <libslic3r/Model.hpp>
|
|
#include <libslic3r/PresetBundle.hpp>
|
|
#include <libslic3r/TriangleMesh.hpp>
|
|
#include <slic3r/GUI/DockPanel.hpp>
|
|
#include <slic3r/GUI/AuiPaneLayout.hpp>
|
|
#include <slic3r/GUI/Widgets/WebHosting.hpp>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/catch_message.hpp>
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include "plugin_test_utils.hpp"
|
|
#include <pybind11/pytypes.h>
|
|
#include <pybind11/cast.h>
|
|
#include "python_test_support.hpp"
|
|
|
|
#include <pybind11/embed.h>
|
|
#include <pybind11/pybind11.h>
|
|
|
|
#include <string>
|
|
|
|
#include <wx/string.h>
|
|
#include <wx/uri.h>
|
|
#include "libslic3r/ObjectID.hpp"
|
|
#include "libslic3r/Point.hpp"
|
|
#include "libslic3r/Preset.hpp"
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
#include <pybind11/detail/common.h>
|
|
|
|
namespace py = pybind11;
|
|
|
|
namespace {
|
|
|
|
// import_orca_module() lives in python_test_support.hpp (shared with
|
|
// test_slicing_pipeline_bindings.cpp).
|
|
|
|
bool has_attr(const py::handle& object, const char* name)
|
|
{
|
|
return py::hasattr(object, name);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Python", "[PluginHost][Python]")
|
|
{
|
|
py::module_ orca = import_orca_module();
|
|
REQUIRE(has_attr(orca, "host"));
|
|
|
|
py::object host = orca.attr("host");
|
|
REQUIRE(has_attr(host, "PresetBundle"));
|
|
REQUIRE(has_attr(host, "Preset"));
|
|
REQUIRE(has_attr(host, "PresetCollection"));
|
|
REQUIRE(has_attr(host, "Model"));
|
|
REQUIRE(has_attr(host, "ModelObject"));
|
|
REQUIRE(has_attr(host, "Plater"));
|
|
|
|
py::object preset_bundle_type = host.attr("PresetBundle");
|
|
CHECK(has_attr(preset_bundle_type, "prints"));
|
|
CHECK(has_attr(preset_bundle_type, "printers"));
|
|
CHECK(has_attr(preset_bundle_type, "filaments"));
|
|
CHECK(has_attr(preset_bundle_type, "current_process_preset"));
|
|
CHECK(has_attr(preset_bundle_type, "current_printer_preset"));
|
|
CHECK(has_attr(preset_bundle_type, "current_filament_preset_names"));
|
|
CHECK(has_attr(preset_bundle_type, "current_filament_presets"));
|
|
CHECK(has_attr(preset_bundle_type, "full_config_value"));
|
|
|
|
py::object preset_collection_type = host.attr("PresetCollection");
|
|
CHECK(has_attr(preset_collection_type, "get_edited_preset"));
|
|
CHECK(has_attr(preset_collection_type, "get_selected_preset"));
|
|
CHECK(has_attr(preset_collection_type, "get_selected_preset_name"));
|
|
CHECK(has_attr(preset_collection_type, "edited_preset"));
|
|
CHECK(has_attr(preset_collection_type, "selected_preset"));
|
|
CHECK(has_attr(preset_collection_type, "selected_preset_name"));
|
|
|
|
py::object preset_type = host.attr("Preset");
|
|
CHECK(has_attr(preset_type, "name"));
|
|
CHECK(has_attr(preset_type, "type"));
|
|
CHECK(has_attr(preset_type, "is_default"));
|
|
CHECK(has_attr(preset_type, "is_system"));
|
|
CHECK(has_attr(preset_type, "is_user"));
|
|
CHECK(has_attr(preset_type, "is_from_bundle"));
|
|
CHECK(has_attr(preset_type, "config_value"));
|
|
|
|
Slic3r::PresetBundle bundle;
|
|
Slic3r::Preset& printer_preset = bundle.printers.get_edited_preset();
|
|
Slic3r::Preset& process_preset = bundle.prints.get_edited_preset();
|
|
Slic3r::Preset& filament_preset = bundle.filaments.get_edited_preset();
|
|
|
|
printer_preset.config.set("printer_model", "Plugin Host Test Printer", true);
|
|
bundle.filament_presets = { filament_preset.name, filament_preset.name, "missing filament preset" };
|
|
|
|
py::object py_bundle = py::cast(&bundle, py::return_value_policy::reference);
|
|
|
|
CHECK(py_bundle.attr("current_printer_preset")().attr("name").cast<std::string>() == printer_preset.name);
|
|
CHECK(py_bundle.attr("current_print_preset")().attr("name").cast<std::string>() == process_preset.name);
|
|
CHECK(py_bundle.attr("current_process_preset")().attr("name").cast<std::string>() == process_preset.name);
|
|
CHECK(py_bundle.attr("current_printer_preset")().attr("is_default").cast<bool>() == printer_preset.is_default);
|
|
CHECK(py_bundle.attr("current_printer_preset")().attr("is_user")().cast<bool>() == printer_preset.is_user());
|
|
CHECK(py_bundle.attr("current_printer_preset")().attr("config_value")("printer_model").cast<std::string>() == "Plugin Host Test Printer");
|
|
CHECK(py_bundle.attr("current_printer_preset")().attr("config_value")("missing_test_key").is_none());
|
|
|
|
py::list filament_names = py_bundle.attr("current_filament_preset_names")();
|
|
REQUIRE(py::len(filament_names) == 3);
|
|
CHECK(filament_names[0].cast<std::string>() == filament_preset.name);
|
|
CHECK(filament_names[1].cast<std::string>() == filament_preset.name);
|
|
CHECK(filament_names[2].cast<std::string>() == "missing filament preset");
|
|
|
|
py::list filament_presets = py_bundle.attr("current_filament_presets")();
|
|
REQUIRE(py::len(filament_presets) == 3);
|
|
CHECK_FALSE(filament_presets[0].is_none());
|
|
CHECK(filament_presets[0].attr("name").cast<std::string>() == filament_preset.name);
|
|
CHECK_FALSE(filament_presets[1].is_none());
|
|
CHECK(filament_presets[1].attr("name").cast<std::string>() == filament_preset.name);
|
|
CHECK(filament_presets[2].is_none());
|
|
|
|
py::object printers = py_bundle.attr("printers");
|
|
py::object prints = py_bundle.attr("prints");
|
|
py::object filaments = py_bundle.attr("filaments");
|
|
CHECK(printers.attr("get_edited_preset")().attr("name").cast<std::string>() == printer_preset.name);
|
|
CHECK(prints.attr("get_edited_preset")().attr("name").cast<std::string>() == process_preset.name);
|
|
CHECK(filaments.attr("get_edited_preset")().attr("name").cast<std::string>() == filament_preset.name);
|
|
CHECK(printers.attr("get_selected_preset_name")().cast<std::string>() == bundle.printers.get_selected_preset_name());
|
|
CHECK(printers.attr("get_selected_preset")().attr("name").cast<std::string>() == bundle.printers.get_selected_preset().name);
|
|
CHECK(printers.attr("selected_preset_name")().cast<std::string>() == bundle.printers.get_selected_preset_name());
|
|
CHECK(printers.attr("edited_preset")().attr("name").cast<std::string>() == printer_preset.name);
|
|
CHECK(printers.attr("find_preset")(printer_preset.name).attr("name").cast<std::string>() == printer_preset.name);
|
|
}
|
|
|
|
TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHost][Python]")
|
|
{
|
|
py::object host = import_orca_module().attr("host");
|
|
|
|
for (const char* function_name : { "preset_bundle", "plater", "model" }) {
|
|
CAPTURE(function_name);
|
|
try {
|
|
host.attr(function_name)();
|
|
FAIL("host accessor unexpectedly succeeded without a wx application");
|
|
} catch (const py::error_already_set& error) {
|
|
CHECK(error.matches(PyExc_RuntimeError));
|
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app initialization", "[PluginHost][Python]")
|
|
{
|
|
py::object host = import_orca_module().attr("host");
|
|
REQUIRE(has_attr(host, "ui"));
|
|
|
|
py::object ui = host.attr("ui");
|
|
CHECK(has_attr(ui, "message"));
|
|
CHECK_FALSE(has_attr(ui, "show_dialog"));
|
|
CHECK(has_attr(ui, "create_window"));
|
|
CHECK(has_attr(ui, "WINDOW_MODELESS"));
|
|
CHECK(has_attr(ui, "WINDOW_MODAL"));
|
|
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
|
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
|
CHECK(has_attr(ui, "UiWindow"));
|
|
CHECK(has_attr(ui, "create_dock_panel"));
|
|
CHECK(has_attr(ui, "UiDockPanel"));
|
|
|
|
// With no wx application the UI calls marshal to a main thread that does not
|
|
// exist here; they must fail cleanly with a clear error, not crash.
|
|
try {
|
|
ui.attr("message")("hello");
|
|
FAIL("orca.host.ui.message unexpectedly succeeded without a wx application");
|
|
} catch (const py::error_already_set& error) {
|
|
CHECK(error.matches(PyExc_RuntimeError));
|
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
|
}
|
|
|
|
try {
|
|
ui.attr("create_dock_panel")("<p>panel</p>");
|
|
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
|
} catch (const py::error_already_set& error) {
|
|
CHECK(error.matches(PyExc_RuntimeError));
|
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
|
}
|
|
|
|
// Positional arguments follow create_window(): width and height come straight after the title.
|
|
try {
|
|
ui.attr("create_dock_panel")("<p>panel</p>", "Panel", 400, 300);
|
|
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
|
} catch (const py::error_already_set& error) {
|
|
CHECK(error.matches(PyExc_RuntimeError));
|
|
}
|
|
|
|
// An unknown dock position is rejected before the application is needed.
|
|
try {
|
|
ui.attr("create_dock_panel")("<p>panel</p>", py::arg("dock") = "top");
|
|
FAIL("orca.host.ui.create_dock_panel accepted an unknown dock position");
|
|
} catch (const py::error_already_set& error) {
|
|
CHECK(error.matches(PyExc_ValueError));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Plugin pane names identify the plugin and title without layout delimiters", "[PluginHost]")
|
|
{
|
|
using Slic3r::GUI::plugin_pane_name;
|
|
|
|
CHECK(plugin_pane_name("dock_demo", "Scene") == "plugin:dock_demo:Scene");
|
|
CHECK(plugin_pane_name("key", "a|b;c=d\\e").find_first_of("|;=\\") == std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("A pane's saved layout entry is found by pane name", "[PluginHost]")
|
|
{
|
|
using Slic3r::GUI::aui_pane_layout_entry;
|
|
|
|
const std::string sidebar = "name=sidebar;caption=;state=2099196;dir=4;layer=0;row=0;pos=0;bestw=390;besth=900";
|
|
// The caption holds an escaped '|', which must not end the entry.
|
|
const std::string plugin = "name=plugin:demo:Scene;caption=Scene \\| stats;state=2099198;dir=2;layer=0;row=1;pos=0;bestw=320;besth=480";
|
|
const std::string layout = "layout3|" + sidebar + "|" + plugin + "|dock_size(4,0,0)=392|";
|
|
|
|
CHECK(aui_pane_layout_entry(layout, "plugin:demo:Scene") == plugin);
|
|
CHECK(aui_pane_layout_entry(layout, "sidebar") == sidebar);
|
|
CHECK(aui_pane_layout_entry(layout, "plugin:demo").empty());
|
|
CHECK(aui_pane_layout_entry("", "plugin:demo:Scene").empty());
|
|
}
|
|
|
|
TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside", "[PluginHost]")
|
|
{
|
|
using namespace Slic3r::GUI::web_hosting;
|
|
|
|
// A resources path holding a space, which the web view may report escaped differently.
|
|
const Slic3r::ScopedResourcesDir resources("web content check");
|
|
|
|
// The swapped-in page, then after an in-page anchor and a reload.
|
|
CHECK(is_content_url(content_base_url()));
|
|
CHECK(is_content_url(content_base_url() + "#tab2"));
|
|
const wxString unescaped = wxURI::Unescape(content_base_url());
|
|
REQUIRE(unescaped != content_base_url());
|
|
CHECK(is_content_url(unescaped));
|
|
CHECK(is_content_url(unescaped + "#tab2"));
|
|
// A page the plugin linked to keeps its own URL and must be left alone.
|
|
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
|
CHECK_FALSE(is_content_url("https://example.com/"));
|
|
CHECK_FALSE(is_content_url(""));
|
|
}
|
|
|
|
TEST_CASE("A reloaded plugin page is recognised when the resources path holds a '#'", "[PluginHost]")
|
|
{
|
|
using namespace Slic3r::GUI::web_hosting;
|
|
|
|
const Slic3r::ScopedResourcesDir resources("web#content check");
|
|
|
|
CHECK(is_content_url(content_base_url()));
|
|
CHECK(is_content_url(content_base_url() + "#tab2"));
|
|
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
|
}
|
|
|
|
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
|
{
|
|
using Catch::Matchers::WithinAbs;
|
|
using Catch::Matchers::WithinRel;
|
|
|
|
py::object host = import_orca_module().attr("host");
|
|
REQUIRE(has_attr(host, "BoundingBox"));
|
|
REQUIRE(has_attr(host, "Model"));
|
|
REQUIRE(has_attr(host, "ModelInstance"));
|
|
REQUIRE(has_attr(host, "ModelVolume"));
|
|
REQUIRE(has_attr(host, "ModelVolumeType"));
|
|
|
|
py::object volume_type_enum = host.attr("ModelVolumeType");
|
|
CHECK(has_attr(volume_type_enum, "ModelPart"));
|
|
CHECK(has_attr(volume_type_enum, "ParameterModifier"));
|
|
CHECK(has_attr(volume_type_enum, "SupportEnforcer"));
|
|
|
|
// Build a model in C++: one object with a 10x20x30 mm printable part, a small
|
|
// modifier volume, and a single instance shifted on the bed.
|
|
Slic3r::Model model;
|
|
Slic3r::ModelObject* object = model.add_object();
|
|
object->name = "Plugin Host Test Cube";
|
|
|
|
Slic3r::ModelVolume* part = object->add_volume(Slic3r::make_cube(10.0, 20.0, 30.0));
|
|
part->name = "cube part";
|
|
Slic3r::ModelVolume* modifier = object->add_volume(Slic3r::make_cube(2.0, 2.0, 2.0),
|
|
Slic3r::ModelVolumeType::PARAMETER_MODIFIER);
|
|
modifier->name = "fit modifier";
|
|
|
|
Slic3r::ModelInstance* instance = object->add_instance();
|
|
instance->set_offset(Slic3r::Vec3d(5.0, 6.0, 0.0));
|
|
|
|
py::object py_model = py::cast(&model, py::return_value_policy::reference);
|
|
|
|
// Model surface.
|
|
CHECK(py_model.attr("object_count")().cast<size_t>() == 1);
|
|
CHECK(py_model.attr("id")().cast<size_t>() == model.id().id);
|
|
CHECK(py_model.attr("bounding_box")().attr("defined").cast<bool>());
|
|
|
|
// Object surface.
|
|
py::object py_object = py_model.attr("object")(0);
|
|
CHECK(py_object.attr("name").cast<std::string>() == "Plugin Host Test Cube");
|
|
CHECK(py_object.attr("id")().cast<size_t>() == object->id().id);
|
|
CHECK(py_object.attr("instance_count")().cast<size_t>() == 1);
|
|
CHECK(py_object.attr("volume_count")().cast<size_t>() == 2);
|
|
CHECK(py::len(py_object.attr("instances")()) == 1);
|
|
CHECK(py::len(py_object.attr("volumes")()) == 2);
|
|
CHECK(py_object.attr("is_multiparts")().cast<bool>());
|
|
|
|
// Intrinsic (untransformed) object size must match the printable part's dimensions.
|
|
py::object obj_size = py_object.attr("raw_mesh_bounding_box")().attr("size");
|
|
REQUIRE_THAT(obj_size[py::int_(0)].cast<double>(), WithinAbs(10.0, 1e-3));
|
|
REQUIRE_THAT(obj_size[py::int_(1)].cast<double>(), WithinAbs(20.0, 1e-3));
|
|
REQUIRE_THAT(obj_size[py::int_(2)].cast<double>(), WithinAbs(30.0, 1e-3));
|
|
|
|
// Instance surface.
|
|
py::object py_instance = py_object.attr("instance")(0);
|
|
py::object inst_offset = py_instance.attr("offset")();
|
|
REQUIRE_THAT(inst_offset[py::int_(0)].cast<double>(), WithinAbs(5.0, 1e-6));
|
|
REQUIRE_THAT(inst_offset[py::int_(1)].cast<double>(), WithinAbs(6.0, 1e-6));
|
|
REQUIRE_THAT(inst_offset[py::int_(2)].cast<double>(), WithinAbs(0.0, 1e-6));
|
|
CHECK(py_instance.attr("id")().cast<size_t>() == instance->id().id);
|
|
|
|
// Volume surface — part.
|
|
py::object py_part = py_object.attr("volume")(0);
|
|
CHECK(py_part.attr("name").cast<std::string>() == "cube part");
|
|
CHECK(py_part.attr("is_model_part")().cast<bool>());
|
|
CHECK_FALSE(py_part.attr("is_modifier")().cast<bool>());
|
|
CHECK(py_part.attr("type")().cast<Slic3r::ModelVolumeType>() == Slic3r::ModelVolumeType::MODEL_PART);
|
|
CHECK(py_part.attr("facets_count")().cast<size_t>() == 12);
|
|
REQUIRE_THAT(py_part.attr("volume")().cast<double>(), WithinRel(6000.0, 1e-2));
|
|
|
|
// Volume surface — modifier.
|
|
py::object py_modifier = py_object.attr("volume")(1);
|
|
CHECK(py_modifier.attr("is_modifier")().cast<bool>());
|
|
CHECK_FALSE(py_modifier.attr("is_model_part")().cast<bool>());
|
|
CHECK(py_modifier.attr("type")().cast<Slic3r::ModelVolumeType>() == Slic3r::ModelVolumeType::PARAMETER_MODIFIER);
|
|
}
|
|
|
|
TEST_CASE("Plugin host API exposes TriangleMesh geometry to Python", "[PluginHost][Python]")
|
|
{
|
|
using Catch::Matchers::WithinAbs;
|
|
using Catch::Matchers::WithinRel;
|
|
|
|
py::object host = import_orca_module().attr("host");
|
|
REQUIRE(has_attr(host, "TriangleMesh"));
|
|
|
|
py::object mesh_type = host.attr("TriangleMesh");
|
|
for (const char* member : { "vertex_count", "triangle_count", "facets_count", "is_empty",
|
|
"vertices", "triangles", "face_normals", "vertex", "triangle",
|
|
"volume", "bounding_box", "is_manifold" }) {
|
|
CAPTURE(member);
|
|
CHECK(has_attr(mesh_type, member));
|
|
}
|
|
|
|
// A 10 x 20 x 30 mm box: 8 vertices, 12 triangles.
|
|
Slic3r::Model model;
|
|
Slic3r::ModelObject* object = model.add_object();
|
|
object->add_volume(Slic3r::make_cube(10.0, 20.0, 30.0));
|
|
Slic3r::ModelInstance* instance = object->add_instance();
|
|
instance->set_offset(Slic3r::Vec3d(5.0, 6.0, 0.0));
|
|
|
|
py::object py_object = py::cast(object, py::return_value_policy::reference);
|
|
py::object py_volume = py_object.attr("volume")(0);
|
|
py::object mesh = py_volume.attr("mesh")();
|
|
|
|
// Deterministic, numpy-free surface.
|
|
CHECK(mesh.attr("vertex_count")().cast<size_t>() == 8);
|
|
CHECK(mesh.attr("triangle_count")().cast<size_t>() == 12);
|
|
CHECK(mesh.attr("facets_count")().cast<size_t>() == 12);
|
|
CHECK_FALSE(mesh.attr("is_empty")().cast<bool>());
|
|
CHECK(mesh.attr("is_manifold")().cast<bool>());
|
|
REQUIRE_THAT(mesh.attr("volume")().cast<double>(), WithinRel(6000.0, 1e-2));
|
|
|
|
py::object bbox_size = mesh.attr("bounding_box")().attr("size");
|
|
REQUIRE_THAT(bbox_size[py::int_(0)].cast<double>(), WithinAbs(10.0, 1e-3));
|
|
REQUIRE_THAT(bbox_size[py::int_(1)].cast<double>(), WithinAbs(20.0, 1e-3));
|
|
REQUIRE_THAT(bbox_size[py::int_(2)].cast<double>(), WithinAbs(30.0, 1e-3));
|
|
|
|
py::object vertex0 = mesh.attr("vertex")(0);
|
|
REQUIRE(py::len(vertex0) == 3);
|
|
py::object triangle0 = mesh.attr("triangle")(0);
|
|
REQUIRE(py::len(triangle0) == 3);
|
|
for (int k = 0; k < 3; ++k) {
|
|
int idx = triangle0[py::int_(k)].cast<int>();
|
|
CHECK(idx >= 0);
|
|
CHECK(idx < 8);
|
|
}
|
|
CHECK_THROWS_AS(mesh.attr("vertex")(8), py::error_already_set);
|
|
CHECK_THROWS_AS(mesh.attr("triangle")(12), py::error_already_set);
|
|
|
|
// numpy path: exercised when numpy is importable, otherwise assert the clear
|
|
// "numpy required" error so the absent path is itself covered.
|
|
bool have_numpy = false;
|
|
try {
|
|
py::module_::import("numpy");
|
|
have_numpy = true;
|
|
} catch (const py::error_already_set&) {
|
|
have_numpy = false;
|
|
}
|
|
|
|
if (!have_numpy) {
|
|
WARN("numpy unavailable in unit-test interpreter; asserting the numpy-absent error path");
|
|
try {
|
|
mesh.attr("vertices")();
|
|
FAIL("vertices() must raise ImportError when numpy is unavailable");
|
|
} catch (const py::error_already_set& error) {
|
|
CHECK(error.matches(PyExc_ImportError));
|
|
CHECK(std::string(error.what()).find("numpy is required") != std::string::npos);
|
|
}
|
|
return;
|
|
}
|
|
|
|
py::object vertices = mesh.attr("vertices")();
|
|
CHECK(vertices.attr("shape").cast<py::tuple>()[py::int_(0)].cast<size_t>() == 8);
|
|
CHECK(vertices.attr("shape").cast<py::tuple>()[py::int_(1)].cast<size_t>() == 3);
|
|
CHECK(vertices.attr("dtype").attr("name").cast<std::string>() == "float32");
|
|
CHECK_FALSE(vertices.attr("flags").attr("writeable").cast<bool>());
|
|
CHECK_FALSE(vertices.attr("base").is_none()); // zero-copy view keeps an owner alive
|
|
CHECK_THROWS_AS(vertices.attr("__setitem__")(py::make_tuple(0, 0), py::float_(1.0)), py::error_already_set);
|
|
|
|
py::object triangles = mesh.attr("triangles")();
|
|
CHECK(triangles.attr("shape").cast<py::tuple>()[py::int_(0)].cast<size_t>() == 12);
|
|
CHECK(triangles.attr("shape").cast<py::tuple>()[py::int_(1)].cast<size_t>() == 3);
|
|
CHECK(triangles.attr("dtype").attr("name").cast<std::string>() == "int32");
|
|
CHECK_FALSE(triangles.attr("flags").attr("writeable").cast<bool>());
|
|
|
|
py::object face_normals = mesh.attr("face_normals")();
|
|
CHECK(face_normals.attr("shape").cast<py::tuple>()[py::int_(0)].cast<size_t>() == 12);
|
|
CHECK(face_normals.attr("dtype").attr("name").cast<std::string>() == "float32");
|
|
|
|
// World-space transform matrices.
|
|
py::object volume_matrix = py_volume.attr("matrix")();
|
|
CHECK(volume_matrix.attr("shape").cast<py::tuple>()[py::int_(0)].cast<size_t>() == 4);
|
|
CHECK(volume_matrix.attr("shape").cast<py::tuple>()[py::int_(1)].cast<size_t>() == 4);
|
|
CHECK(volume_matrix.attr("dtype").attr("name").cast<std::string>() == "float64");
|
|
|
|
py::object instance_matrix = py_object.attr("instance")(0).attr("matrix")();
|
|
CHECK(instance_matrix.attr("shape").cast<py::tuple>()[py::int_(0)].cast<size_t>() == 4);
|
|
// Instance offset (5, 6, 0) must land in the matrix translation column.
|
|
REQUIRE_THAT(instance_matrix.attr("__getitem__")(py::make_tuple(0, 3)).cast<double>(), WithinAbs(5.0, 1e-6));
|
|
REQUIRE_THAT(instance_matrix.attr("__getitem__")(py::make_tuple(1, 3)).cast<double>(), WithinAbs(6.0, 1e-6));
|
|
}
|