diff --git a/.gitignore b/.gitignore index 31f7492..ba129da 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist/ *.pyc __pycache__/ .cache +*.egg-info # Editor and OS things imgui.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index e4ebd7f..320877d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,8 +17,14 @@ pybind11_add_module(polyscope_bindings src/cpp/point_cloud.cpp src/cpp/curve_network.cpp src/cpp/volume_mesh.cpp + src/cpp/volume_grid.cpp + src/cpp/camera_view.cpp + src/cpp/floating_quantities.cpp + src/cpp/implicit_helpers.cpp + src/cpp/managed_buffer.cpp src/cpp/imgui.cpp ) +set_target_properties(polyscope_bindings PROPERTIES CXX_VISIBILITY_PRESET "default") target_include_directories(polyscope_bindings PUBLIC "${EIGEN3_INCLUDE_DIR}") target_link_libraries(polyscope_bindings PRIVATE polyscope) diff --git a/deps/polyscope b/deps/polyscope index 5b052a6..07c5695 160000 --- a/deps/polyscope +++ b/deps/polyscope @@ -1 +1 @@ -Subproject commit 5b052a62bdc5fb020d7e1ae00cf834295c607f8c +Subproject commit 07c5695708b88014c3a87a80b480a077419adf00 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..4d2050e --- /dev/null +++ b/ruff.toml @@ -0,0 +1,10 @@ + +# Never enforce +# - `E501` line length violations) +# - `E701` statement after colon on same line +ignore = ["E501","E701"] + +# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. +[per-file-ignores] +"__init__.py" = ["E402"] +"path/to/file.py" = ["E402"] diff --git a/src/cpp/camera_view.cpp b/src/cpp/camera_view.cpp new file mode 100644 index 0000000..f64fb24 --- /dev/null +++ b/src/cpp/camera_view.cpp @@ -0,0 +1,49 @@ +#include +#include +#include + +#include "Eigen/Dense" + +#include "polyscope/camera_view.h" +#include "polyscope/polyscope.h" + +#include "utils.h" + +namespace py = pybind11; +namespace ps = polyscope; + + +// clang-format off +void bind_camera_view(py::module& m) { + + // == Helper quantity classes + + // == Main class + bindStructure(m, "CameraView") + + // basics + .def("get_camera_parameters", &ps::CameraView::getCameraParameters, "Get camera parameters") + .def("update_camera_parameters", &ps::CameraView::updateCameraParameters, "Update camera parameters") + + // options + .def("set_widget_color", &ps::CameraView::setWidgetColor, "Set color") + .def("get_widget_color", &ps::CameraView::getWidgetColor, "Get color") + .def("set_widget_thickness", &ps::CameraView::setWidgetThickness, "Set widget thickness") + .def("get_widget_thickness", &ps::CameraView::getWidgetThickness, "Get widget thickness") + .def("set_widget_focal_length", &ps::CameraView::setWidgetFocalLength, "Set widget focal length") + .def("get_widget_focal_length", &ps::CameraView::getWidgetFocalLength, "Get widget focal length") + + // camera controls + .def("set_view_to_this_camera", &ps::CameraView::setViewToThisCamera, "Set view to this camera") + ; + + + // Static adders and getters + m.def("register_camera_view", &ps::registerCameraView, + py::arg("name"), py::arg("parameters"), "Register a camera view", py::return_value_policy::reference); + m.def("remove_camera_view", &ps::removeCameraView, "Remove a camera view by name"); + m.def("get_camera_view", &ps::getCameraView, "Get a camera view by name", py::return_value_policy::reference); + m.def("has_camera_view", &ps::hasCameraView, "Check for a camera view by name"); + +} +// clang-format on diff --git a/src/cpp/core.cpp b/src/cpp/core.cpp index 9446dfd..de446a7 100644 --- a/src/cpp/core.cpp +++ b/src/cpp/core.cpp @@ -7,15 +7,19 @@ #include "Eigen/Dense" #include "polyscope/affine_remapper.h" +#include "polyscope/camera_parameters.h" #include "polyscope/curve_network.h" +#include "polyscope/messages.h" #include "polyscope/pick.h" #include "polyscope/point_cloud.h" #include "polyscope/polyscope.h" #include "polyscope/surface_mesh.h" -#include "polyscope/surface_parameterization_enums.h" +#include "polyscope/types.h" #include "polyscope/view.h" #include "polyscope/volume_mesh.h" +#include "utils.h" + namespace py = pybind11; namespace ps = polyscope; @@ -29,6 +33,11 @@ void bind_surface_mesh(py::module& m); void bind_point_cloud(py::module& m); void bind_curve_network(py::module& m); void bind_volume_mesh(py::module& m); +void bind_volume_grid(py::module& m); +void bind_camera_view(py::module& m); +void bind_floating_quantities(py::module& m); +void bind_implicit_helpers(py::module& m); +void bind_managed_buffer(py::module& m); void bind_imgui(py::module& m); // Signal handler (makes ctrl-c work, etc) @@ -58,8 +67,8 @@ PYBIND11_MODULE(polyscope_bindings, m) { // === Basic flow m.def("init", &ps::init, py::arg("backend")="", "Initialize Polyscope"); - m.def("checkInitialized", &ps::checkInitialized); - m.def("isInitialized", &ps::isInitialized); + m.def("check_initialized", &ps::checkInitialized); + m.def("is_initialized", &ps::isInitialized); m.def("show", [](size_t forFrames) { if (ps::state::userCallback == nullptr) { bool oldVal = ps::options::openImGuiWindowForUserCallback; @@ -77,6 +86,15 @@ PYBIND11_MODULE(polyscope_bindings, m) { }, py::arg("forFrames")=std::numeric_limits::max() ); + m.def("unshow", &ps::unshow); + m.def("frame_tick", &ps::frameTick); + + // === Render engine related things + m.def("get_render_engine_backend_name", &ps::render::getRenderEngineBackendName); + m.def("get_key_code", [](char c) { + ps::checkInitialized(); + return ps::render::engine->getKeyCode(c); + }); // === Structure management m.def("remove_all_structures", &ps::removeAllStructures, "Remove all structures from polyscope"); @@ -90,19 +108,25 @@ PYBIND11_MODULE(polyscope_bindings, m) { m.def("set_program_name", [](std::string x) { ps::options::programName = x; }); m.def("set_verbosity", [](int x) { ps::options::verbosity = x; }); m.def("set_print_prefix", [](std::string x) { ps::options::printPrefix = x; }); - m.def("set_max_fps", [](int x) { ps::options::maxFPS = x; }); m.def("set_errors_throw_exceptions", [](bool x) { ps::options::errorsThrowExceptions = x; }); + m.def("set_max_fps", [](int x) { ps::options::maxFPS = x; }); + m.def("set_enable_vsync", [](bool x) { ps::options::enableVSync = x; }); m.def("set_use_prefs_file", [](bool x) { ps::options::usePrefsFile = x; }); + m.def("request_redraw", []() { ps::requestRedraw(); }); + m.def("get_redraw_requested", []() { return ps::redrawRequested(); }); m.def("set_always_redraw", [](bool x) { ps::options::alwaysRedraw = x; }); m.def("set_enable_render_error_checks", [](bool x) { ps::options::enableRenderErrorChecks = x; }); m.def("set_autocenter_structures", [](bool x) { ps::options::autocenterStructures = x; }); m.def("set_autoscale_structures", [](bool x) { ps::options::autoscaleStructures = x; }); m.def("set_build_gui", [](bool x) { ps::options::buildGui = x; }); + m.def("set_user_gui_is_on_right_side", [](bool x) { ps::options::userGuiIsOnRightSide = x; }); + m.def("set_build_default_gui_panels", [](bool x) { ps::options::buildDefaultGuiPanels = x; }); + m.def("set_render_scene", [](bool x) { ps::options::renderScene = x; }); m.def("set_open_imgui_window_for_user_callback", [](bool x) { ps::options::openImGuiWindowForUserCallback= x; }); m.def("set_invoke_user_callback_for_nested_show", [](bool x) { ps::options::invokeUserCallbackForNestedShow = x; }); m.def("set_give_focus_on_show", [](bool x) { ps::options::giveFocusOnShow = x; }); - m.def("set_navigation_style", [](ps::view::NavigateStyle x) { ps::view::style = x; }); - m.def("set_up_dir", [](ps::view::UpDir x) { ps::view::setUpDir(x); }); + m.def("set_hide_window_after_show", [](bool x) { ps::options::hideWindowAfterShow = x; }); + // === Scene extents m.def("set_automatically_compute_scene_extents", [](bool x) { ps::options::automaticallyComputeSceneExtents = x; }); @@ -111,7 +135,14 @@ PYBIND11_MODULE(polyscope_bindings, m) { m.def("set_bounding_box", [](glm::vec3 low, glm::vec3 high) { ps::state::boundingBox = std::tuple(low, high); }); m.def("get_bounding_box", []() { return ps::state::boundingBox; }); - // === Camera controls + // === Camera controls & View + m.def("set_navigation_style", [](ps::view::NavigateStyle x) { ps::view::style = x; }); + m.def("get_navigation_style", ps::view::getNavigateStyle); + m.def("set_up_dir", [](ps::UpDir x) { ps::view::setUpDir(x); }); + m.def("get_up_dir", ps::view::getUpDir); + m.def("set_front_dir", [](ps::FrontDir x) { ps::view::setFrontDir(x); }); + m.def("get_front_dir", ps::view::getFrontDir); + m.def("reset_camera_to_home_view", ps::view::resetCameraToHomeView); m.def("look_at", [](glm::vec3 location, glm::vec3 target, bool flyTo) { ps::view::lookAt(location, target, flyTo); @@ -120,6 +151,28 @@ PYBIND11_MODULE(polyscope_bindings, m) { ps::view::lookAt(location, target, upDir, flyTo); }); m.def("set_view_projection_mode", [](ps::ProjectionMode x) { ps::view::projectionMode = x; }); + m.def("get_view_camera_parameters", &ps::view::getCameraParametersForCurrentView); + m.def("set_view_camera_parameters", &ps::view::setViewToCamera); + m.def("set_camera_view_matrix", [](Eigen::Matrix4f mat) { ps::view::setCameraViewMatrix(eigen2glm(mat)); }); + m.def("get_camera_view_matrix", []() { return glm2eigen(ps::view::getCameraViewMatrix()); }); + m.def("set_window_size", &ps::view::setWindowSize); + m.def("get_window_size", &ps::view::getWindowSize); + m.def("get_buffer_size", &ps::view::getBufferSize); + m.def("set_window_resizable", &ps::view::setWindowResizable); + m.def("get_window_resizable", &ps::view::getWindowResizable); + m.def("set_view_from_json", ps::view::setViewFromJson); + m.def("get_view_as_json", ps::view::getViewAsJson); + m.def("set_background_color", [](glm::vec4 c) { for(int i = 0; i < 4; i++) ps::view::bgColor[i] = c[i]; }); + m.def("get_background_color", []() { return glm2eigen(glm::vec4{ + ps::view::bgColor[0], ps::view::bgColor[1], ps::view::bgColor[2], ps::view::bgColor[3] + }); + }); + + // === "Advanced" UI management + m.def("build_polyscope_gui", &ps::buildPolyscopeGui); + m.def("build_structure_gui", &ps::buildStructureGui); + m.def("build_pick_gui", &ps::buildPickGui); + m.def("build_user_gui_and_invoke_callback", &ps::buildUserGuiAndInvokeCallback); // === Messages m.def("info", ps::info, "Send an info message"); @@ -194,6 +247,39 @@ PYBIND11_MODULE(polyscope_bindings, m) { // === Rendering m.def("set_SSAA_factor", [](int n) { ps::options::ssaaFactor = n; }); + + + // === Structure + + // (this is the generic structure class, subtypes get bound in their respective files) + py::class_(m, "Structure") + .def_readonly("name", &ps::Structure::name) + ; + + // === Groups + + py::class_(m, "Group") + .def(py::init()) + .def_readonly("name", &ps::Group::name) + .def("add_child_group", &ps::Group::addChildGroup) + .def("add_child_structure", &ps::Group::addChildStructure) + .def("remove_child_group", &ps::Group::removeChildGroup) + .def("remove_child_structure", &ps::Group::removeChildStructure) + .def("set_enabled", &ps::Group::setEnabled, py::return_value_policy::reference) + .def("set_show_child_details", &ps::Group::setShowChildDetails) + .def("set_hide_descendants_from_structure_lists", &ps::Group::setHideDescendantsFromStructureLists) + ; + + // create/get/delete + m.def("create_group", &ps::createGroup, py::return_value_policy::reference); + m.def("get_group", &ps::getGroup, py::return_value_policy::reference); + m.def("remove_group", overload_cast_()(&ps::removeGroup)); + m.def("remove_all_groups", &ps::removeAllGroups); + + + // === Low-level internals access + // (warning, 'advanced' users only, may change) + m.def("get_final_scene_color_texture_native_handle", []() { ps::render::engine->getFinalSceneColorTexture().getNativeBufferID(); }); // === Slice planes py::class_(m, "SlicePlane") @@ -212,6 +298,65 @@ PYBIND11_MODULE(polyscope_bindings, m) { m.def("add_scene_slice_plane", ps::addSceneSlicePlane, "add a slice plane", py::return_value_policy::reference); m.def("remove_last_scene_slice_plane", ps::removeLastSceneSlicePlane, "remove last scene plane"); + // === Camera Parameters + py::class_(m, "CameraIntrinsics") + .def(py::init<>()) + .def_static("from_FoV_deg_vertical_and_aspect", &ps::CameraIntrinsics::fromFoVDegVerticalAndAspect) + .def_static("from_FoV_deg_horizontal_and_aspect", &ps::CameraIntrinsics::fromFoVDegHorizontalAndAspect) + .def_static("from_FoV_deg_horizontal_and_vertical", &ps::CameraIntrinsics::fromFoVDegHorizontalAndVertical) + ; + py::class_(m, "CameraExtrinsics") + .def(py::init<>()) + .def_static("from_vectors", &ps::CameraExtrinsics::fromVectors) + .def_static("from_matrix", [](Eigen::Matrix4f mat) { return ps::CameraExtrinsics::fromMatrix(eigen2glm(mat)); }) + ; + py::class_(m, "CameraParameters") + .def(py::init()) + .def("get_intrinsics", [](ps::CameraParameters& c) { return c.intrinsics; }) + .def("get_extrinsics", [](ps::CameraParameters& c) { return c.extrinsics; }) + .def("get_T", [](ps::CameraParameters& c) { return glm2eigen(c.getT()); }) + .def("get_R", [](ps::CameraParameters& c) { return glm2eigen(c.getR()); }) + .def("get_view_mat", [](ps::CameraParameters& c) { return glm2eigen(c.getViewMat()); }) + .def("get_E", [](ps::CameraParameters& c) { return glm2eigen(c.getE()); }) + .def("get_position", [](ps::CameraParameters& c) { return glm2eigen(c.getPosition()); }) + .def("get_look_dir", [](ps::CameraParameters& c) { return glm2eigen(c.getLookDir()); }) + .def("get_up_dir", [](ps::CameraParameters& c) { return glm2eigen(c.getUpDir()); }) + .def("get_right_dir", [](ps::CameraParameters& c) { return glm2eigen(c.getRightDir()); }) + .def("get_camera_frame", [](ps::CameraParameters& c) { + return std::make_tuple(glm2eigen(c.getLookDir()), glm2eigen(c.getUpDir()), glm2eigen(c.getRightDir())); + }) + .def("get_fov_vertical_deg", &ps::CameraParameters::getFoVVerticalDegrees) + .def("get_aspect", &ps::CameraParameters::getAspectRatioWidthOverHeight) + .def("generate_camera_rays", [](ps::CameraParameters& c, size_t dimX, size_t dimY, ps::ImageOrigin origin) { + std::vector rays = c.generateCameraRays(dimX, dimY, origin); + Eigen::MatrixXf raysOut(rays.size(), 3); + for(size_t i = 0; i < rays.size(); i++) { + raysOut(i,0) = rays[i].x; + raysOut(i,1) = rays[i].y; + raysOut(i,2) = rays[i].z; + } + return raysOut; + }) + .def("generate_camera_ray_corners", [](ps::CameraParameters& c) { + std::array rays = c.generateCameraRayCorners(); + Eigen::MatrixXf raysOut(4, 3); + for(size_t i = 0; i < rays.size(); i++) { + raysOut(i,0) = rays[i].x; + raysOut(i,1) = rays[i].y; + raysOut(i,2) = rays[i].z; + } + return raysOut; + }) + ; + + // === Weak Handles + // (used for lifetime tracking tricks) + py::class_(m, "GenericWeakHandle") + .def(py::init<>()) + .def("is_valid", &ps::GenericWeakHandle::isValid) + .def("get_unique_ID", &ps::GenericWeakHandle::getUniqueID) + ; + // === Enums py::enum_(m, "NavigateStyle") @@ -219,6 +364,8 @@ PYBIND11_MODULE(polyscope_bindings, m) { .value("free", ps::view::NavigateStyle::Free) .value("planar", ps::view::NavigateStyle::Planar) .value("arcball", ps::view::NavigateStyle::Arcball) + .value("none", ps::view::NavigateStyle::None) + .value("first_person", ps::view::NavigateStyle::FirstPerson) .export_values(); py::enum_(m, "ProjectionMode") @@ -226,13 +373,22 @@ PYBIND11_MODULE(polyscope_bindings, m) { .value("orthographic", ps::ProjectionMode::Orthographic) .export_values(); - py::enum_(m, "UpDir") - .value("x_up", ps::view::UpDir::XUp) - .value("y_up", ps::view::UpDir::YUp) - .value("z_up", ps::view::UpDir::ZUp) - .value("neg_x_up", ps::view::UpDir::NegXUp) - .value("neg_y_up", ps::view::UpDir::NegYUp) - .value("neg_z_up", ps::view::UpDir::NegZUp) + py::enum_(m, "UpDir") + .value("x_up", ps::UpDir::XUp) + .value("y_up", ps::UpDir::YUp) + .value("z_up", ps::UpDir::ZUp) + .value("neg_x_up", ps::UpDir::NegXUp) + .value("neg_y_up", ps::UpDir::NegYUp) + .value("neg_z_up", ps::UpDir::NegZUp) + .export_values(); + + py::enum_(m, "FrontDir") + .value("x_front", ps::FrontDir::XFront) + .value("y_front", ps::FrontDir::YFront) + .value("z_front", ps::FrontDir::ZFront) + .value("neg_x_front", ps::FrontDir::NegXFront) + .value("neg_y_front", ps::FrontDir::NegYFront) + .value("neg_z_front", ps::FrontDir::NegZFront) .export_values(); py::enum_(m, "DataType") @@ -253,6 +409,7 @@ PYBIND11_MODULE(polyscope_bindings, m) { py::enum_(m, "ParamVizStyle") .value("checker", ps::ParamVizStyle::CHECKER) + .value("checker_islands", ps::ParamVizStyle::CHECKER_ISLANDS) .value("grid", ps::ParamVizStyle::GRID) .value("local_check", ps::ParamVizStyle::LOCAL_CHECK) .value("local_rad", ps::ParamVizStyle::LOCAL_RAD) @@ -282,6 +439,45 @@ PYBIND11_MODULE(polyscope_bindings, m) { .value("sphere", ps::PointRenderMode::Sphere) .value("quad", ps::PointRenderMode::Quad) .export_values(); + + py::enum_(m, "ImageOrigin") + .value("lower_left", ps::ImageOrigin::LowerLeft) + .value("upper_left", ps::ImageOrigin::UpperLeft) + .export_values(); + + py::enum_(m, "MeshShadeStyle") + .value("smooth", ps::MeshShadeStyle::Smooth) + .value("flat", ps::MeshShadeStyle::Flat) + .value("tri_flat", ps::MeshShadeStyle::TriFlat) + .export_values(); + + py::enum_(m, "ImplicitRenderMode") + .value("sphere_march", ps::ImplicitRenderMode::SphereMarch) + .value("fixed_step", ps::ImplicitRenderMode::FixedStep) + .export_values(); + + py::enum_(m, "ManagedBufferType") + .value(ps::typeName(ps::ManagedBufferType::Float ).c_str(), ps::ManagedBufferType::Float ) + .value(ps::typeName(ps::ManagedBufferType::Double ).c_str(), ps::ManagedBufferType::Double ) + .value(ps::typeName(ps::ManagedBufferType::Vec2 ).c_str(), ps::ManagedBufferType::Vec2 ) + .value(ps::typeName(ps::ManagedBufferType::Vec3 ).c_str(), ps::ManagedBufferType::Vec3 ) + .value(ps::typeName(ps::ManagedBufferType::Vec4 ).c_str(), ps::ManagedBufferType::Vec4 ) + .value(ps::typeName(ps::ManagedBufferType::Arr2Vec3).c_str(), ps::ManagedBufferType::Arr2Vec3) + .value(ps::typeName(ps::ManagedBufferType::Arr3Vec3).c_str(), ps::ManagedBufferType::Arr3Vec3) + .value(ps::typeName(ps::ManagedBufferType::Arr4Vec3).c_str(), ps::ManagedBufferType::Arr4Vec3) + .value(ps::typeName(ps::ManagedBufferType::UInt32 ).c_str(), ps::ManagedBufferType::UInt32 ) + .value(ps::typeName(ps::ManagedBufferType::Int32 ).c_str(), ps::ManagedBufferType::Int32 ) + .value(ps::typeName(ps::ManagedBufferType::UVec2 ).c_str(), ps::ManagedBufferType::UVec2 ) + .value(ps::typeName(ps::ManagedBufferType::UVec3 ).c_str(), ps::ManagedBufferType::UVec3 ) + .value(ps::typeName(ps::ManagedBufferType::UVec4 ).c_str(), ps::ManagedBufferType::UVec4 ) + .export_values(); + + py::enum_(m, "DeviceBufferType") + .value("attribute", ps::DeviceBufferType::Attribute) + .value("texture1d", ps::DeviceBufferType::Texture1d) + .value("texture2d", ps::DeviceBufferType::Texture2d) + .value("texture3d", ps::DeviceBufferType::Texture3d) + .export_values(); // === Mini bindings for a little bit of glm py::class_(m, "glm_vec3"). @@ -297,12 +493,25 @@ PYBIND11_MODULE(polyscope_bindings, m) { [](const glm::vec4& x) { return std::tuple(x[0], x[1], x[2], x[3]); }); + + py::class_(m, "glm_uvec3"). + def(py::init()) + .def("as_tuple", + [](const glm::uvec3& x) { + return std::tuple(x[0], x[1], x[2]); + }); + // === Bind structures defined in other files - bind_surface_mesh(m); + bind_floating_quantities(m); + bind_implicit_helpers(m); bind_point_cloud(m); bind_curve_network(m); + bind_surface_mesh(m); bind_volume_mesh(m); + bind_volume_grid(m); + bind_camera_view(m); + bind_managed_buffer(m); bind_imgui(m); } diff --git a/src/cpp/curve_network.cpp b/src/cpp/curve_network.cpp index 998df2a..4392a91 100644 --- a/src/cpp/curve_network.cpp +++ b/src/cpp/curve_network.cpp @@ -35,8 +35,8 @@ void bind_curve_network(py::module& m) { bindStructure(m, "CurveNetwork") // basics - .def("update_node_positions", &ps::CurveNetwork::updateNodePositions, "Update node positions") - .def("update_node_positions2D", &ps::CurveNetwork::updateNodePositions2D, "Update node positions") + .def("update_node_positions", &ps::CurveNetwork::updateNodePositions, "Update node positions") + .def("update_node_positions2D", &ps::CurveNetwork::updateNodePositions2D, "Update node positions") .def("n_nodes", &ps::CurveNetwork::nNodes, "# nodes") .def("n_edges", &ps::CurveNetwork::nEdges, "# edges") @@ -48,50 +48,43 @@ void bind_curve_network(py::module& m) { .def("set_material", &ps::CurveNetwork::setMaterial, "Set material") .def("get_material", &ps::CurveNetwork::getMaterial, "Get material") - // slice planes - .def("set_ignore_slice_plane", &ps::CurveNetwork::setIgnoreSlicePlane, "Set ignore slice plane") - .def("get_ignore_slice_plane", &ps::CurveNetwork::getIgnoreSlicePlane, "Get ignore slice plane") - .def("set_cull_whole_elements", &ps::CurveNetwork::setCullWholeElements, "Set cull whole elements") - .def("get_cull_whole_elements", &ps::CurveNetwork::getCullWholeElements, "Get cull whole elements") - - // quantities - .def("add_node_color_quantity", &ps::CurveNetwork::addNodeColorQuantity, "Add a color function at nodes", + .def("add_node_color_quantity", &ps::CurveNetwork::addNodeColorQuantity, "Add a color function at nodes", py::arg("name"), py::arg("values"), py::return_value_policy::reference) - .def("add_edge_color_quantity", &ps::CurveNetwork::addEdgeColorQuantity, "Add a color function at edges", + .def("add_edge_color_quantity", &ps::CurveNetwork::addEdgeColorQuantity, "Add a color function at edges", py::arg("name"), py::arg("values"), py::return_value_policy::reference) - .def("add_node_scalar_quantity", &ps::CurveNetwork::addNodeScalarQuantity, "Add a scalar function at nodes", + .def("add_node_scalar_quantity", &ps::CurveNetwork::addNodeScalarQuantity, "Add a scalar function at nodes", py::arg("name"), py::arg("values"), py::arg("data_type")=ps::DataType::STANDARD, py::return_value_policy::reference) - .def("add_edge_scalar_quantity", &ps::CurveNetwork::addEdgeScalarQuantity, "Add a scalar function at edge", + .def("add_edge_scalar_quantity", &ps::CurveNetwork::addEdgeScalarQuantity, "Add a scalar function at edge", py::arg("name"), py::arg("values"), py::arg("data_type")=ps::DataType::STANDARD, py::return_value_policy::reference) - .def("add_node_vector_quantity", &ps::CurveNetwork::addNodeVectorQuantity, "Add a vector function at nodes", + .def("add_node_vector_quantity", &ps::CurveNetwork::addNodeVectorQuantity, "Add a vector function at nodes", py::arg("name"), py::arg("values"), py::arg("vector_type")=ps::VectorType::STANDARD, py::return_value_policy::reference) - .def("add_node_vector_quantity2D", &ps::CurveNetwork::addNodeVectorQuantity2D, "Add a vector function at nodes", + .def("add_node_vector_quantity2D", &ps::CurveNetwork::addNodeVectorQuantity2D, "Add a vector function at nodes", py::arg("name"), py::arg("values"), py::arg("vector_type")=ps::VectorType::STANDARD, py::return_value_policy::reference) - .def("add_edge_vector_quantity", &ps::CurveNetwork::addEdgeVectorQuantity, "Add a vector function at edges", + .def("add_edge_vector_quantity", &ps::CurveNetwork::addEdgeVectorQuantity, "Add a vector function at edges", py::arg("name"), py::arg("values"), py::arg("vector_type")=ps::VectorType::STANDARD, py::return_value_policy::reference) - .def("add_edge_vector_quantity2D", &ps::CurveNetwork::addEdgeVectorQuantity2D, "Add a vector function at edges", + .def("add_edge_vector_quantity2D", &ps::CurveNetwork::addEdgeVectorQuantity2D, "Add a vector function at edges", py::arg("name"), py::arg("values"), py::arg("vector_type")=ps::VectorType::STANDARD, py::return_value_policy::reference); // Static adders and getters - m.def("register_curve_network", &ps::registerCurveNetwork, + m.def("register_curve_network", &ps::registerCurveNetwork, py::arg("name"), py::arg("nodes"), py::arg("edges"), "Register a curve network", py::return_value_policy::reference); - m.def("register_curve_network2D", &ps::registerCurveNetwork2D, + m.def("register_curve_network2D", &ps::registerCurveNetwork2D, py::arg("name"), py::arg("nodes"), py::arg("edges"), "Register a curve network", py::return_value_policy::reference); - m.def("register_curve_network_line", &ps::registerCurveNetworkLine, + m.def("register_curve_network_line", &ps::registerCurveNetworkLine, py::arg("name"), py::arg("nodes"), "Register a curve network", py::return_value_policy::reference); - m.def("register_curve_network_line2D", &ps::registerCurveNetworkLine2D, + m.def("register_curve_network_line2D", &ps::registerCurveNetworkLine2D, py::arg("name"), py::arg("nodes"), "Register a curve network", py::return_value_policy::reference); - m.def("register_curve_network_loop", &ps::registerCurveNetworkLoop, + m.def("register_curve_network_loop", &ps::registerCurveNetworkLoop, py::arg("name"), py::arg("nodes"), "Register a curve network", py::return_value_policy::reference); - m.def("register_curve_network_loop2D", &ps::registerCurveNetworkLoop2D, + m.def("register_curve_network_loop2D", &ps::registerCurveNetworkLoop2D, py::arg("name"), py::arg("nodes"), "Register a curve network", py::return_value_policy::reference); diff --git a/src/cpp/floating_quantities.cpp b/src/cpp/floating_quantities.cpp new file mode 100644 index 0000000..33d1c41 --- /dev/null +++ b/src/cpp/floating_quantities.cpp @@ -0,0 +1,87 @@ +#include +#include +#include + +#include "Eigen/Dense" + +#include "polyscope/polyscope.h" + +#include "polyscope/floating_quantities.h" +#include "polyscope/image_quantity.h" + +#include "utils.h" + +namespace py = pybind11; +namespace ps = polyscope; + +// clang-format off +void bind_floating_quantities(py::module& m) { + + // == Global floating quantity management + + // global floating quantity structure + bindStructure(m, "FloatingQuantityStructure"); + m.def("get_global_floating_quantity_structure", &ps::getGlobalFloatingQuantityStructure, py::return_value_policy::reference); + + m.def("remove_floating_quantity", &ps::removeFloatingQuantity); + m.def("remove_all_floating_quantities", &ps::removeAllFloatingQuantities); + + // == Image floating quantities + + auto qScalarImage = bindScalarQuantity(m, "ScalarImageQuantity"); + addImageQuantityBindings(qScalarImage); + + auto qColorImage = bindColorQuantity(m, "ColorImageQuantity"); + addImageQuantityBindings(qColorImage); + qColorImage.def("set_is_premultiplied", &ps::ColorImageQuantity::setIsPremultiplied); + + // global / free-floating adders + m.def("add_scalar_image_quantity", &ps::addScalarImageQuantity, + py::return_value_policy::reference); + m.def("add_color_image_quantity", &ps::addColorImageQuantity, + py::return_value_policy::reference); + m.def("add_color_alpha_image_quantity", &ps::addColorAlphaImageQuantity, + py::return_value_policy::reference); + + // == Render image floating quantities + + auto qDepthRenderImage = bindQuantity(m, "DepthRenderImageQuantity"); + qDepthRenderImage.def("set_material", &ps::DepthRenderImageQuantity::setMaterial, "Set material"); + qDepthRenderImage.def("set_color", &ps::DepthRenderImageQuantity::setColor, "Set color"); + qDepthRenderImage.def("set_transparency", &ps::DepthRenderImageQuantity::setTransparency, "Set transparency"); + qDepthRenderImage.def("set_allow_fullscreen_compositing", &ps::DepthRenderImageQuantity::setAllowFullscreenCompositing); + + auto qColorRenderImage = bindColorQuantity(m, "ColorRenderImageQuantity"); + qColorRenderImage.def("set_material", &ps::ColorRenderImageQuantity::setMaterial, "Set material"); + qColorRenderImage.def("set_transparency", &ps::ColorRenderImageQuantity::setTransparency, "Set transparency"); + qColorRenderImage.def("set_allow_fullscreen_compositing", &ps::ColorRenderImageQuantity::setAllowFullscreenCompositing); + + auto qScalarRenderImage = bindScalarQuantity(m, "ScalarRenderImageQuantity"); + qScalarRenderImage.def("set_material", &ps::ScalarRenderImageQuantity::setMaterial, "Set material"); + qScalarRenderImage.def("set_transparency", &ps::ScalarRenderImageQuantity::setTransparency, "Set transparency"); + qScalarRenderImage.def("set_allow_fullscreen_compositing", &ps::ScalarRenderImageQuantity::setAllowFullscreenCompositing); + + auto qRawColorRenderImage = bindColorQuantity(m, "RawColorRenderImageQuantity"); + qRawColorRenderImage.def("set_transparency", &ps::RawColorRenderImageQuantity::setTransparency, "Set transparency"); + qRawColorRenderImage.def("set_allow_fullscreen_compositing", &ps::RawColorRenderImageQuantity::setAllowFullscreenCompositing); + + auto qRawColorAlphaRenderImage = bindColorQuantity(m, "RawColorAlphaRenderImageQuantity"); + qRawColorAlphaRenderImage.def("set_transparency", &ps::RawColorAlphaRenderImageQuantity::setTransparency, "Set transparency"); + qRawColorAlphaRenderImage.def("set_is_premultiplied", &ps::RawColorAlphaRenderImageQuantity::setIsPremultiplied); + qRawColorAlphaRenderImage.def("set_allow_fullscreen_compositing", &ps::RawColorAlphaRenderImageQuantity::setAllowFullscreenCompositing); + + // global / free-floating adders + m.def("add_depth_render_image_quantity", &ps::addDepthRenderImageQuantity, + py::return_value_policy::reference); + m.def("add_color_render_image_quantity", &ps::addColorRenderImageQuantity, + py::return_value_policy::reference); + m.def("add_scalar_render_image_quantity", &ps::addScalarRenderImageQuantity, + py::return_value_policy::reference); + m.def("add_raw_color_render_image_quantity", &ps::addRawColorRenderImageQuantity, + py::return_value_policy::reference); + m.def("add_raw_color_alpha_render_image_quantity", &ps::addRawColorAlphaRenderImageQuantity, + py::return_value_policy::reference); + +} + +// clang-format on diff --git a/src/cpp/imgui.cpp b/src/cpp/imgui.cpp index 76bc8c4..9c399b8 100644 --- a/src/cpp/imgui.cpp +++ b/src/cpp/imgui.cpp @@ -1,5 +1,7 @@ #include "imgui.h" + #include +#include #include #include @@ -50,17 +52,122 @@ static int input_text_callback(ImGuiInputTextCallbackData* data) { } +void bind_imgui_structs(py::module& m); void bind_imgui_methods(py::module& m); void bind_imgui_enums(py::module& m); void bind_imgui(py::module& m) { auto imgui_module = m.def_submodule("imgui", "ImGui bindings"); + bind_imgui_structs(imgui_module); bind_imgui_methods(imgui_module); bind_imgui_enums(imgui_module); } // clang-format off + + +// clang-format off +void bind_imgui_structs(py::module& m) { + + // ImGuiIO + py::class_(m, "ImGuiIO") + .def_readwrite("DisplaySize" ,&ImGuiIO::DisplaySize ) + .def_readwrite("DeltaTime" ,&ImGuiIO::DeltaTime ) + .def_readwrite("IniSavingRate" ,&ImGuiIO::IniSavingRate ) + .def_readwrite("IniFilename" ,&ImGuiIO::IniFilename ) + .def_readwrite("MouseDoubleClickTime" ,&ImGuiIO::MouseDoubleClickTime ) + .def_readwrite("MouseDoubleClickMaxDist" ,&ImGuiIO::MouseDoubleClickMaxDist ) + .def_readwrite("MouseDragThreshold" ,&ImGuiIO::MouseDragThreshold ) + .def_property_readonly("KeyMap" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{ImGuiKey_COUNT, o.KeyMap, ob};}) + .def_readwrite("KeyRepeatDelay" ,&ImGuiIO::KeyRepeatDelay ) + .def_readwrite("KeyRepeatRate" ,&ImGuiIO::KeyRepeatRate ) + .def_readwrite("Fonts" ,&ImGuiIO::Fonts ) + .def_readwrite("FontGlobalScale" ,&ImGuiIO::FontGlobalScale ) + .def_readwrite("FontAllowUserScaling" ,&ImGuiIO::FontAllowUserScaling ) + .def_readwrite("FontDefault" ,&ImGuiIO::FontDefault ) + .def_readwrite("DisplayFramebufferScale" ,&ImGuiIO::DisplayFramebufferScale ) + .def_readwrite("MouseDrawCursor" ,&ImGuiIO::MouseDrawCursor ) + .def_readwrite("ConfigMacOSXBehaviors" ,&ImGuiIO::ConfigMacOSXBehaviors ) + .def_readwrite("ConfigInputTextCursorBlink" ,&ImGuiIO::ConfigInputTextCursorBlink ) + .def_readwrite("ConfigDragClickToInputText" ,&ImGuiIO::ConfigDragClickToInputText ) + .def_readwrite("ConfigWindowsResizeFromEdges" ,&ImGuiIO::ConfigWindowsResizeFromEdges ) + .def_readwrite("ConfigWindowsMoveFromTitleBarOnly" ,&ImGuiIO::ConfigWindowsMoveFromTitleBarOnly ) + .def_readwrite("ConfigMemoryCompactTimer" ,&ImGuiIO::ConfigMemoryCompactTimer ) + .def_readwrite("MousePos" ,&ImGuiIO::MousePos ) + .def_property_readonly("MouseDown" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDown , ob};}) + .def_readwrite("MouseWheel" ,&ImGuiIO::MouseWheel ) + .def_readwrite("MouseWheelH" ,&ImGuiIO::MouseWheelH ) + .def_readwrite("KeyCtrl" ,&ImGuiIO::KeyCtrl ) + .def_readwrite("KeyShift" ,&ImGuiIO::KeyShift ) + .def_readwrite("KeyAlt" ,&ImGuiIO::KeyAlt ) + .def_readwrite("KeySuper" ,&ImGuiIO::KeySuper ) + .def_property_readonly("KeysDown" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{512, o.KeysDown , ob};}) + .def_property_readonly("NavInputs" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{ImGuiNavInput_COUNT, o.NavInputs , ob};}) + .def_readwrite("WantCaptureMouse" ,&ImGuiIO::WantCaptureMouse ) + .def_readwrite("WantCaptureKeyboard" ,&ImGuiIO::WantCaptureKeyboard ) + .def_readwrite("WantTextInput" ,&ImGuiIO::WantTextInput ) + .def_readwrite("WantSetMousePos" ,&ImGuiIO::WantSetMousePos ) + .def_readwrite("WantSaveIniSettings" ,&ImGuiIO::WantSaveIniSettings ) + .def_readwrite("NavActive" ,&ImGuiIO::NavActive ) + .def_readwrite("NavVisible" ,&ImGuiIO::NavVisible ) + .def_readwrite("Framerate" ,&ImGuiIO::Framerate ) + .def_readwrite("MetricsRenderVertices" ,&ImGuiIO::MetricsRenderVertices ) + .def_readwrite("MetricsRenderIndices" ,&ImGuiIO::MetricsRenderIndices ) + .def_readwrite("MetricsRenderWindows" ,&ImGuiIO::MetricsRenderWindows ) + .def_readwrite("MetricsActiveWindows" ,&ImGuiIO::MetricsActiveWindows ) + .def_readwrite("MetricsActiveAllocations" ,&ImGuiIO::MetricsActiveAllocations ) + .def_readwrite("MouseDelta" ,&ImGuiIO::MouseDelta ) + .def_readwrite("WantCaptureMouseUnlessPopupClose" ,&ImGuiIO::WantCaptureMouseUnlessPopupClose ) + .def_readwrite("KeyMods" ,&ImGuiIO::KeyMods ) + .def_readwrite("KeyModsPrev" ,&ImGuiIO::KeyModsPrev ) + .def_readwrite("MousePosPrev" ,&ImGuiIO::MousePosPrev ) + .def_property_readonly("MouseClickedPos" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseClickedPos, ob};}) + .def_property_readonly("MouseClickedTime" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseClickedTime, ob};}) + .def_property_readonly("MouseClicked" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseClicked, ob};}) + .def_property_readonly("MouseDoubleClicked" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDoubleClicked, ob};}) + .def_property_readonly("MouseClickedCount" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseClickedCount, ob};}) + .def_property_readonly("MouseClickedLastCount" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseClickedLastCount, ob};}) + .def_property_readonly("MouseReleased" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseReleased, ob};}) + .def_property_readonly("MouseDownOwned" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDownOwned, ob};}) + .def_property_readonly("MouseDownOwnedUnlessPopupClose" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDownOwnedUnlessPopupClose, ob};}) + .def_property_readonly("MouseDownDuration" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDownDuration, ob};}) + .def_property_readonly("MouseDownDurationPrev" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDownDurationPrev, ob};}) + .def_property_readonly("MouseDragMaxDistanceAbs" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDragMaxDistanceAbs, ob};}) + .def_property_readonly("MouseDragMaxDistanceSqr" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{5, o.MouseDragMaxDistanceSqr, ob};}) + .def_property_readonly("KeysDownDuration" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{512, o.KeysDownDuration, ob};}) + .def_property_readonly("KeysDownDurationPrev" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{512, o.KeysDownDurationPrev, ob};}) + .def_property_readonly("NavInputsDownDuration" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{ImGuiNavInput_COUNT, o.NavInputsDownDuration, ob};}) + .def_property_readonly("NavInputsDownDurationPrev" , [](py::object& ob) { ImGuiIO& o = ob.cast(); return py::array{ImGuiNavInput_COUNT, o.NavInputsDownDurationPrev, ob};}) + .def_readwrite("PenPressure" ,&ImGuiIO::PenPressure ) + .def_readwrite("AppFocusLost" ,&ImGuiIO::AppFocusLost ) + .def_readwrite("InputQueueSurrogate" ,&ImGuiIO::InputQueueSurrogate ) + .def_readwrite("InputQueueCharacters" ,&ImGuiIO::InputQueueCharacters ) + + ; + + + py::class_(m, "ImFontAtlas") + .def("AddFontFromFileTTF", + [](py::object& ob, std::string filename, float size_pixels) { ImFontAtlas& o = ob.cast(); return o.AddFontFromFileTTF(filename.c_str(), size_pixels);}, + py::return_value_policy::reference) + + // TODO add bindings to the rest of the font functions + + ; + + py::class_(m, "ImFont") + + // TODO add bindings to the rest of the font functions + + ; + +} + void bind_imgui_methods(py::module& m) { + + // Main + m.def("GetIO", &ImGui::GetIO, py::return_value_policy::reference); + // Windows m.def( "Begin", @@ -232,6 +339,16 @@ void bind_imgui_methods(py::module& m) { py::arg("center_y_ratio") = 0.5f); // Parameters stacks (shared) + IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font + IMGUI_API void PopFont(); + m.def( + "PushFont", + [](ImFont* font) { ImGui::PushFont(font); }, + py::arg("font")); + m.def( + "PopFont", + []() { ImGui::PopFont(); } + ); m.def( "PushStyleColor", [](ImGuiCol idx, ImU32 col) { ImGui::PushStyleColor(idx, col); }, @@ -269,7 +386,7 @@ void bind_imgui_methods(py::module& m) { py::arg("idx"), py::arg("alpha_mul") = 1.0f); m.def( - "GetColorU32", [](const ImVec4& col) { return ImGui::GetColorU32(col); }, py::arg("col")); + "GetColorU32", [](const Vec4T& col) { return ImGui::GetColorU32(to_vec4(col)); }, py::arg("col")); m.def( "GetColorU32", [](ImU32 col) { return ImGui::GetColorU32(col); }, py::arg("col")); @@ -344,11 +461,19 @@ void bind_imgui_methods(py::module& m) { m.def("GetFrameHeightWithSpacing", []() { return ImGui::GetFrameHeightWithSpacing(); }); // ID stack/scopes + m.def( + "PushID", [](const char* str_id) { ImGui::PushID(str_id); }, py::arg("str_id")); + m.def( + "PushID", [](int int_id) { ImGui::PushID(int_id); }, py::arg("int_id")); + m.def("PopID", []() { ImGui::PopID(); }); + m.def( + "GetID", [](const char* str_id) { return ImGui::GetID(str_id); }, py::arg("str_id")); + + // these are typos (bad capitalization). kept around to avoid needless breaking changes m.def( "PushId", [](const char* str_id) { ImGui::PushID(str_id); }, py::arg("str_id")); m.def( "PushId", [](int int_id) { ImGui::PushID(int_id); }, py::arg("int_id")); - m.def("PopID", []() { ImGui::PopID(); }); m.def( "GetId", [](const char* str_id) { return ImGui::GetID(str_id); }, py::arg("str_id")); @@ -494,8 +619,8 @@ void bind_imgui_methods(py::module& m) { py::arg("label"), py::arg("v"), py::arg("v_speed") = 1.0f, - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); m.def( @@ -514,8 +639,8 @@ void bind_imgui_methods(py::module& m) { py::arg("label"), py::arg("v"), py::arg("v_speed") = 1.0f, - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); m.def( @@ -534,8 +659,8 @@ void bind_imgui_methods(py::module& m) { py::arg("label"), py::arg("v"), py::arg("v_speed") = 1.0f, - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); m.def( @@ -554,8 +679,8 @@ void bind_imgui_methods(py::module& m) { py::arg("label"), py::arg("v"), py::arg("v_speed") = 1.0f, - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); m.def( @@ -584,8 +709,8 @@ void bind_imgui_methods(py::module& m) { py::arg("v_current_min"), py::arg("v_current_max"), py::arg("v_speed") = 1.0f, - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("format_max") = nullptr, py::arg("power") = 1.0f); @@ -691,8 +816,8 @@ void bind_imgui_methods(py::module& m) { }, py::arg("label"), py::arg("v"), - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); m.def( @@ -708,8 +833,8 @@ void bind_imgui_methods(py::module& m) { }, py::arg("label"), py::arg("v"), - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); m.def( @@ -725,8 +850,8 @@ void bind_imgui_methods(py::module& m) { }, py::arg("label"), py::arg("v"), - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); m.def( @@ -742,8 +867,8 @@ void bind_imgui_methods(py::module& m) { }, py::arg("label"), py::arg("v"), - py::arg("v_min") = 0.0f, - py::arg("v_max") = 0.0f, + py::arg("v_min"), + py::arg("v_max"), py::arg("format") = "%.3f", py::arg("power") = 1.0f); @@ -1374,6 +1499,13 @@ void bind_imgui_methods(py::module& m) { m.def("LogFinish", []() { ImGui::LogFinish(); }); m.def("LogButtons", []() { ImGui::LogButtons(); }); + // Disabling + m.def("BeginDisabled", + [](bool disable) { + return ImGui::BeginDisabled(disable); + }, py::arg("disable")); + m.def("EndDisabled", []() { ImGui::EndDisabled(); }); + // Clipping m.def( "PushClipRect", @@ -1512,7 +1644,7 @@ void bind_imgui_methods(py::module& m) { // Inputs Utilities: Keyboard m.def("GetKeyIndex", [](ImGuiKey imgui_key) { return ImGui::GetKeyIndex(imgui_key); }, py::arg("imgui_key")); m.def("IsKeyDown", [](ImGuiKey user_key_index) { return ImGui::IsKeyDown(user_key_index); }, py::arg("user_key_index")); - m.def("IsKeyPressed", [](ImGuiKey user_key_index) { return ImGui::IsKeyPressed(user_key_index); }, py::arg("user_key_index")); + m.def("IsKeyPressed", [](ImGuiKey user_key_index, bool repeat) { return ImGui::IsKeyPressed(user_key_index, repeat); }, py::arg("user_key_index"), py::arg("repeat")=true); m.def("IsKeyReleased", [](ImGuiKey user_key_index) { return ImGui::IsKeyReleased(user_key_index); }, py::arg("user_key_index")); m.def( "GetKeyPressedAmount", @@ -1540,6 +1672,217 @@ void bind_imgui_methods(py::module& m) { m.def("LoadIniSettingsFromMemory", [](const char *ini_data) { ImGui::LoadIniSettingsFromMemory(ini_data); }, py::arg("ini_data")); m.def("SaveIniSettingsToDisk", [](const char *ini_filename) { ImGui::SaveIniSettingsToDisk(ini_filename); }, py::arg("ini_filename")); m.def("SaveIniSettingsToMemory", []() { return ImGui::SaveIniSettingsToMemory(); }); + + // Draw Commands + m.def( + "AddLine", + [](const Vec2T& p1, const Vec2T& p2, ImU32 col, float thickness) { + ImGui::GetWindowDrawList()->AddRect(to_vec2(p1), to_vec2(p2), col, thickness); + }, + py::arg("p_min"), + py::arg("p_max"), + py::arg("col"), + py::arg("thickness") = 1.0f + ); + m.def( + "AddRect", + [](const Vec2T& p_min, const Vec2T& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) { + ImGui::GetWindowDrawList()->AddRect(to_vec2(p_min), to_vec2(p_max), col, rounding, flags, thickness); + }, + py::arg("p_min"), + py::arg("p_max"), + py::arg("col"), + py::arg("rounding") = 0.0f, + py::arg("flags") = 0, + py::arg("thickness") = 1.0f + ); + m.def( + "AddRectFilled", + [](const Vec2T& p_min, const Vec2T& p_max, ImU32 col, float rounding, ImDrawFlags flags) { + ImGui::GetWindowDrawList()->AddRectFilled(to_vec2(p_min), to_vec2(p_max), col, rounding, flags); + }, + py::arg("p_min"), + py::arg("p_max"), + py::arg("col"), + py::arg("rounding") = 0.0f, + py::arg("flags") = 0 + ); + m.def( + "AddRectFilledMultiColor", + [](const Vec2T& p_min, const Vec2T& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) { + ImGui::GetWindowDrawList()->AddRectFilledMultiColor(to_vec2(p_min), to_vec2(p_max), col_upr_left, col_upr_right, col_bot_right, col_bot_left); + }, + py::arg("p_min"), + py::arg("p_max"), + py::arg("col_upr_left"), + py::arg("col_upr_right"), + py::arg("col_bot_right"), + py::arg("col_bot_left") + ); + m.def( + "AddQuad", + [](const Vec2T& p1, const Vec2T& p2, const Vec2T& p3, const Vec2T& p4, ImU32 col, float thickness) { + ImGui::GetWindowDrawList()->AddQuad(to_vec2(p1), to_vec2(p2), to_vec2(p3), to_vec2(p4), col, thickness); + }, + py::arg("p1"), + py::arg("p2"), + py::arg("p3"), + py::arg("p4"), + py::arg("col"), + py::arg("thickness") = 1.0f + ); + m.def( + "AddQuadFilled", + [](const Vec2T& p1, const Vec2T& p2, const Vec2T& p3, const Vec2T& p4, ImU32 col) { + ImGui::GetWindowDrawList()->AddQuadFilled(to_vec2(p1), to_vec2(p2), to_vec2(p3), to_vec2(p4), col); + }, + py::arg("p1"), + py::arg("p2"), + py::arg("p3"), + py::arg("p4"), + py::arg("col") + ); + m.def( + "AddTriangle", + [](const Vec2T& p1, const Vec2T& p2, const Vec2T& p3, ImU32 col, float thickness) { + ImGui::GetWindowDrawList()->AddTriangle(to_vec2(p1), to_vec2(p2), to_vec2(p3), col, thickness); + }, + py::arg("p1"), + py::arg("p2"), + py::arg("p3"), + py::arg("col"), + py::arg("thickness") = 1.0f + ); + m.def( + "AddTriangleFilled", + [](const Vec2T& p1, const Vec2T& p2, const Vec2T& p3, ImU32 col) { + ImGui::GetWindowDrawList()->AddTriangleFilled(to_vec2(p1), to_vec2(p2), to_vec2(p3), col); + }, + py::arg("p1"), + py::arg("p2"), + py::arg("p3"), + py::arg("col") + ); + m.def( + "AddCircle", + [](const Vec2T& center, const float radius, ImU32 col, int num_segments, float thickness) { + ImGui::GetWindowDrawList()->AddCircle(to_vec2(center), radius, col, num_segments, thickness); + }, + py::arg("center"), + py::arg("radius"), + py::arg("col"), + py::arg("num_segments") = 0, + py::arg("thickness") = 1.0f + ); + m.def( + "AddCircleFilled", + [](const Vec2T& center, const float radius, ImU32 col, int num_segments) { + ImGui::GetWindowDrawList()->AddCircleFilled(to_vec2(center), radius, col, num_segments); + }, + py::arg("center"), + py::arg("radius"), + py::arg("col"), + py::arg("num_segments") = 0 + ); + m.def( + "AddNgon", + [](const Vec2T& center, const float radius, ImU32 col, int num_segments, float thickness) { + ImGui::GetWindowDrawList()->AddNgon(to_vec2(center), radius, col, num_segments, thickness); + }, + py::arg("center"), + py::arg("radius"), + py::arg("col"), + py::arg("num_segments") = 0, + py::arg("thickness") = 1.0f + ); + m.def( + "AddNgonFilled", + [](const Vec2T& center, const float radius, ImU32 col, int num_segments) { + ImGui::GetWindowDrawList()->AddNgonFilled(to_vec2(center), radius, col, num_segments); + }, + py::arg("center"), + py::arg("radius"), + py::arg("col"), + py::arg("num_segments") = 0 + ); + m.def( + "AddText", + [](const Vec2T& pos, ImU32 col, const char* text_begin, const char* text_end) { + ImGui::GetWindowDrawList()->AddText(to_vec2(pos), col, text_begin, text_end); + }, + py::arg("pos"), + py::arg("col"), + py::arg("text_begin"), + py::arg("text_end") = nullptr + ); + m.def( + "AddText", + [](const ImFont* font, float font_size, const Vec2T& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width) { + ImGui::GetWindowDrawList()->AddText(font, font_size, to_vec2(pos), col, text_begin, text_end, wrap_width); + }, + py::arg("font"), + py::arg("font_size"), + py::arg("pos"), + py::arg("col"), + py::arg("text_begin"), + py::arg("text_end") = nullptr, + py::arg("wrap_width") = 0.0f + ); + m.def( + "AddPolyline", + [](const std::vector& points, int num_points, ImU32 col, ImDrawFlags flags, float thickness) { + std::vector points_vec2(points.size()); + for (int i = 0; i < points.size(); i++) { + points_vec2[i] = to_vec2(points[i]); + } + ImGui::GetWindowDrawList()->AddPolyline(points_vec2.data(), num_points, col, flags, thickness); + }, + py::arg("points"), + py::arg("num_points"), + py::arg("col"), + py::arg("flags"), + py::arg("thickness") + ); + m.def( + "AddConvexPolyFilled", + [](const std::vector& points, int num_points, ImU32 col) { + std::vector points_vec2(points.size()); + for (int i = 0; i < points.size(); i++) { + points_vec2[i] = to_vec2(points[i]); + } + ImGui::GetWindowDrawList()->AddConvexPolyFilled(points_vec2.data(), num_points, col); + }, + py::arg("points"), + py::arg("num_points"), + py::arg("col") + ); + m.def( + "AddBezierCubic", + [](const Vec2T& p1, const Vec2T& p2, const Vec2T& p3, const Vec2T& p4, ImU32 col, float thickness, int num_segments = 0) { + ImGui::GetWindowDrawList()->AddBezierCubic(to_vec2(p1), to_vec2(p2), to_vec2(p3), to_vec2(p4), col, thickness, num_segments); + }, + py::arg("p1"), + py::arg("p2"), + py::arg("p3"), + py::arg("p4"), + py::arg("col"), + py::arg("thickness"), + py::arg("num_segments") = 0 + ); + m.def( + "AddBezierQuadratic", + [](const Vec2T& p1, const Vec2T& p2, const Vec2T& p3, ImU32 col, float thickness, int num_segments = 0) { + ImGui::GetWindowDrawList()->AddBezierQuadratic(to_vec2(p1), to_vec2(p2), to_vec2(p3), col, thickness, num_segments); + }, + py::arg("p1"), + py::arg("p2"), + py::arg("p3"), + py::arg("col"), + py::arg("thickness"), + py::arg("num_segments") = 0 + ); + + } // clang-format on diff --git a/src/cpp/implicit_helpers.cpp b/src/cpp/implicit_helpers.cpp new file mode 100644 index 0000000..7d818de --- /dev/null +++ b/src/cpp/implicit_helpers.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include +#include + +#include "Eigen/Dense" + +#include "polyscope/polyscope.h" + +#include "polyscope/floating_quantities.h" +#include "polyscope/image_quantity.h" +#include "polyscope/implicit_helpers.h" + +#include "utils.h" + +namespace py = pybind11; +namespace ps = polyscope; + +// For overloaded functions, with C++11 compiler only +template +using overload_cast_ = pybind11::detail::overload_cast_impl; + + +// clang-format off +void bind_implicit_helpers(py::module& m) { + + // == Render implicit surfaces + + py::class_(m, "ImplicitRenderOpts") + .def(py::init<>()) + .def_readwrite("cameraParameters", &ps::ImplicitRenderOpts::cameraParameters) + .def_readwrite("dimX", &ps::ImplicitRenderOpts::dimX) + .def_readwrite("dimY", &ps::ImplicitRenderOpts::dimY) + .def_readwrite("subsampleFactor", &ps::ImplicitRenderOpts::subsampleFactor) + .def("set_missDist", [](ps::ImplicitRenderOpts& o, float val, bool isRelative) { o.missDist.set(val, isRelative); }) + .def("set_hitDist", [](ps::ImplicitRenderOpts& o, float val, bool isRelative) { o.hitDist.set(val, isRelative); }) + .def_readwrite("stepFactor", &ps::ImplicitRenderOpts::stepFactor) + .def_readwrite("normalSampleEps", &ps::ImplicitRenderOpts::normalSampleEps) + .def("set_stepSize", [](ps::ImplicitRenderOpts& o, float val, bool isRelative) { o.stepSize.set(val, isRelative); }) + .def_readwrite("nMaxSteps", &ps::ImplicitRenderOpts::nMaxSteps) + ; + + + // NOTE: we only bind the batch functions, the one-off functions would be brutally slow due to Python overhead + + + m.def("render_implicit_surface_batch", []( + + std::string name, + const std::function)>& func, + ps::ImplicitRenderMode mode, ps::ImplicitRenderOpts opts, ps::CameraView* cameraView + ) { + + // Polyscope's API uses raw buffer pointers, but we use Eigen mats for pybind11. + // Create a wrapper function that goes to/from the Eigen mats + auto wrapped_func = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map mapped_result(result_ptr, size); + mapped_result = func(mapped_pos); + }; + + if(cameraView == nullptr) { + return ps::renderImplicitSurfaceBatch(name, wrapped_func, mode, opts); + } else { + return ps::renderImplicitSurfaceBatch(cameraView, name, wrapped_func, mode, opts); + } + }, py::return_value_policy::reference); + + + m.def("render_implicit_surface_color_batch", []( + std::string name, + const std::function)>& func, + const std::function)>& func_color, + ps::ImplicitRenderMode mode, ps::ImplicitRenderOpts opts, ps::CameraView* cameraView + ) { + + // Polyscope's API uses raw buffer pointers, but we use Eigen mats for pybind11. + // Create a wrapper function that goes to/from the Eigen mats + auto wrapped_func = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map mapped_result(result_ptr, size); + mapped_result = func(mapped_pos); + }; + auto wrapped_func_color = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map> mapped_result(result_ptr, size, 3); + mapped_result = func_color(mapped_pos); + }; + + if(cameraView == nullptr) { + return ps::renderImplicitSurfaceColorBatch(name, wrapped_func, wrapped_func_color, mode, opts); + } else { + return ps::renderImplicitSurfaceColorBatch(cameraView, name, wrapped_func, wrapped_func_color, mode, opts); + } + }, py::return_value_policy::reference); + + + m.def("render_implicit_surface_scalar_batch", []( + std::string name, + const std::function)>& func, + const std::function)>& func_scalar, + ps::ImplicitRenderMode mode, ps::ImplicitRenderOpts opts, ps::CameraView* cameraView + ) { + + // Polyscope's API uses raw buffer pointers, but we use Eigen mats for pybind11. + // Create a wrapper function that goes to/from the Eigen mats + auto wrapped_func = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map mapped_result(result_ptr, size); + mapped_result = func(mapped_pos); + }; + auto wrapped_func_scalar = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map mapped_result(result_ptr, size); + mapped_result = func_scalar(mapped_pos); + }; + + if(cameraView == nullptr) { + return ps::renderImplicitSurfaceScalarBatch(name, wrapped_func, wrapped_func_scalar, mode, opts); + } else { + return ps::renderImplicitSurfaceScalarBatch(cameraView, name, wrapped_func, wrapped_func_scalar, mode, opts); + } + }, py::return_value_policy::reference); + + m.def("render_implicit_surface_raw_color_batch", []( + std::string name, + const std::function)>& func, + const std::function)>& func_color, + ps::ImplicitRenderMode mode, ps::ImplicitRenderOpts opts, ps::CameraView* cameraView + ) { + + // Polyscope's API uses raw buffer pointers, but we use Eigen mats for pybind11. + // Create a wrapper function that goes to/from the Eigen mats + auto wrapped_func = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map mapped_result(result_ptr, size); + mapped_result = func(mapped_pos); + }; + auto wrapped_func_color = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map> mapped_result(result_ptr, size, 3); + mapped_result = func_color(mapped_pos); + }; + + if(cameraView == nullptr) { + return ps::renderImplicitSurfaceRawColorBatch(name, wrapped_func, wrapped_func_color, mode, opts); + } else { + return ps::renderImplicitSurfaceRawColorBatch(cameraView, name, wrapped_func, wrapped_func_color, mode, opts); + } + }, py::return_value_policy::reference); + +} + +// clang-format on diff --git a/src/cpp/managed_buffer.cpp b/src/cpp/managed_buffer.cpp new file mode 100644 index 0000000..f1aceeb --- /dev/null +++ b/src/cpp/managed_buffer.cpp @@ -0,0 +1,208 @@ +#include +#include +#include +#include + +#include "Eigen/Dense" + +#include "polyscope/polyscope.h" + +#include "polyscope/floating_quantities.h" +#include "polyscope/image_quantity.h" + +#include "utils.h" + +namespace py = pybind11; +namespace ps = polyscope; + +template +py::class_> bind_managed_buffer_T(py::module& m, ps::ManagedBufferType t) { + + return py::class_>(m, ("ManagedBuffer_" + ps::typeName(t)).c_str()) + .def("size", &ps::render::ManagedBuffer::size) + .def("get_texture_size", &ps::render::ManagedBuffer::getTextureSize) + .def("has_data", &ps::render::ManagedBuffer::hasData) + .def("summary_string", &ps::render::ManagedBuffer::summaryString) + .def("get_device_buffer_type", &ps::render::ManagedBuffer::getDeviceBufferType) + .def("get_generic_weak_handle", + [](ps::render::ManagedBuffer& s) { + return s.getGenericWeakHandle(); + } /* intentionally let Python manage ownership */) + .def("get_value", overload_cast_()(&ps::render::ManagedBuffer::getValue)) + .def("get_value", overload_cast_()(&ps::render::ManagedBuffer::getValue)) + .def("get_value", overload_cast_()(&ps::render::ManagedBuffer::getValue)) + .def("mark_host_buffer_updated", &ps::render::ManagedBuffer::markHostBufferUpdated) + .def("get_device_buffer_size_in_bytes", + [](ps::render::ManagedBuffer& s) { + // NOTE: this could cause the underlying device buffer to be allocatred if it wasn't already + if (s.getDeviceBufferType() == polyscope::DeviceBufferType::Attribute) { + return s.getRenderAttributeBuffer()->getDataSizeInBytes(); + } else { + return s.getRenderTextureBuffer()->getSizeInBytes(); + } + }) + .def("get_device_buffer_element_size_in_bytes", + [](ps::render::ManagedBuffer& s) { + // NOTE: this could cause the underlying device buffer to be allocatred if it wasn't already + if (s.getDeviceBufferType() == polyscope::DeviceBufferType::Attribute) { + std::shared_ptr buff = s.getRenderAttributeBuffer(); + return polyscope::sizeInBytes(buff->getType()) * buff->getArrayCount(); + } else { + std::shared_ptr buff = s.getRenderTextureBuffer(); + return polyscope::sizeInBytes(buff->getFormat()); + } + }) + .def("get_native_render_attribute_buffer_ID", + [](ps::render::ManagedBuffer& s) { return s.getRenderAttributeBuffer()->getNativeBufferID(); }) + .def("mark_render_attribute_buffer_updated", &ps::render::ManagedBuffer::markRenderAttributeBufferUpdated) + .def("get_native_render_texture_buffer_ID", + [](ps::render::ManagedBuffer& s) { return s.getRenderTextureBuffer()->getNativeBufferID(); }) + .def("mark_render_texture_buffer_updated", &ps::render::ManagedBuffer::markRenderTextureBufferUpdated) + + + ; +} + +// clang-format off +void bind_managed_buffer(py::module& m) { + + // explicit template instantiations + + bind_managed_buffer_T(m, ps::ManagedBufferType::Float) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::VectorXf& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size())); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = d(i); + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::Double) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::VectorXd& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size())); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = d(i); + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::Vec2) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 2"); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = {d(i,0), d(i,1)}; + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::Vec3) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 3"); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = {d(i,0), d(i,1), d(i,2)}; + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::Vec4) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size() || d.cols() != 4) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 4"); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = {d(i,0), d(i,1), d(i,2), d(i,3)}; + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T>(m, ps::ManagedBufferType::Arr2Vec3) + .def("update_data", [](ps::render::ManagedBuffer>& s, std::array,2>& d) { + for(uint32_t k = 0; k < 2; k++) { + if(d[k].rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 3"); + } + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) { + for(uint32_t k = 0; k < 2; k++) { + s.data[i][k] = {d[k](i,0), d[k](i,1), d[k](i,2)}; + } + } + s.markHostBufferUpdated(); + }) + ; + + + bind_managed_buffer_T>(m, ps::ManagedBufferType::Arr3Vec3) + .def("update_data", [](ps::render::ManagedBuffer>& s, std::array,3>& d) { + for(uint32_t k = 0; k < 3; k++) { + if(d[k].rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 3"); + } + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) { + for(uint32_t k = 0; k < 3; k++) { + s.data[i][k] = {d[k](i,0), d[k](i,1), d[k](i,2)}; + } + } + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T>(m, ps::ManagedBufferType::Arr4Vec3) + .def("update_data", [](ps::render::ManagedBuffer>& s, std::array,4>& d) { + for(uint32_t k = 0; k < 4; k++) { + if(d[k].rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 3"); + } + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) { + for(uint32_t k = 0; k < 4; k++) { + s.data[i][k] = {d[k](i,0), d[k](i,1), d[k](i,2)}; + } + } + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::UInt32) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size())); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = d(i); + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::Int32) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size())); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = d(i); + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::UVec2) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 2"); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = {d(i,0), d(i,1)}; + s.markHostBufferUpdated(); + }) + ; + + + bind_managed_buffer_T(m, ps::ManagedBufferType::UVec3) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 3"); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = {d(i,0), d(i,1), d(i,2)}; + s.markHostBufferUpdated(); + }) + ; + + bind_managed_buffer_T(m, ps::ManagedBufferType::UVec4) + .def("update_data", [](ps::render::ManagedBuffer& s, Eigen::Matrix& d) { + if(d.rows() != s.size()) ps::exception("bad update size, should be " + std::to_string(s.size()) + " x 4"); + s.ensureHostBufferAllocated(); + for(uint32_t i = 0; i < s.size(); i++) s.data[i] = {d(i,0), d(i,1), d(i,2), d(i,3)}; + s.markHostBufferUpdated(); + }) + ; + +} diff --git a/src/cpp/point_cloud.cpp b/src/cpp/point_cloud.cpp index b339443..c17218c 100644 --- a/src/cpp/point_cloud.cpp +++ b/src/cpp/point_cloud.cpp @@ -36,8 +36,8 @@ void bind_point_cloud(py::module& m) { bindStructure(m, "PointCloud") // basics - .def("update_point_positions", &ps::PointCloud::updatePointPositions, "Update point positions") - .def("update_point_positions2D", &ps::PointCloud::updatePointPositions2D, "Update point positions") + .def("update_point_positions", &ps::PointCloud::updatePointPositions, "Update point positions") + .def("update_point_positions2D", &ps::PointCloud::updatePointPositions2D, "Update point positions") .def("n_points", &ps::PointCloud::nPoints, "# points") // options @@ -50,13 +50,6 @@ void bind_point_cloud(py::module& m) { .def("set_point_render_mode", &ps::PointCloud::setPointRenderMode, "Set point render mode") .def("get_point_render_mode", &ps::PointCloud::getPointRenderMode, "Get point render mode") - // slice planes - .def("set_ignore_slice_plane", &ps::PointCloud::setIgnoreSlicePlane, "Set ignore slice plane") - .def("get_ignore_slice_plane", &ps::PointCloud::getIgnoreSlicePlane, "Get ignore slice plane") - .def("set_cull_whole_elements", &ps::PointCloud::setCullWholeElements, "Set cull whole elements") - .def("get_cull_whole_elements", &ps::PointCloud::getCullWholeElements, "Get cull whole elements") - - // variable radius .def("set_point_radius_quantity", overload_cast_()(&ps::PointCloud::setPointRadiusQuantity), @@ -67,19 +60,19 @@ void bind_point_cloud(py::module& m) { .def("clear_point_radius_quantity", &ps::PointCloud::clearPointRadiusQuantity, "Clear any quantity setting the radius") // quantities - .def("add_color_quantity", &ps::PointCloud::addColorQuantity, "Add a color function at points", + .def("add_color_quantity", &ps::PointCloud::addColorQuantity, "Add a color function at points", py::arg("name"), py::arg("values"), py::return_value_policy::reference) - .def("add_scalar_quantity", &ps::PointCloud::addScalarQuantity, "Add a scalar function at points", + .def("add_scalar_quantity", &ps::PointCloud::addScalarQuantity, "Add a scalar function at points", py::arg("name"), py::arg("values"), py::arg("data_type")=ps::DataType::STANDARD, py::return_value_policy::reference) - .def("add_vector_quantity", &ps::PointCloud::addVectorQuantity, "Add a vector function at points", + .def("add_vector_quantity", &ps::PointCloud::addVectorQuantity, "Add a vector function at points", py::arg("name"), py::arg("values"), py::arg("vector_type")=ps::VectorType::STANDARD, py::return_value_policy::reference) - .def("add_vector_quantity2D", &ps::PointCloud::addVectorQuantity2D, "Add a vector function at points", + .def("add_vector_quantity2D", &ps::PointCloud::addVectorQuantity2D, "Add a vector function at points", py::arg("name"), py::arg("values"), py::arg("vector_type")=ps::VectorType::STANDARD, py::return_value_policy::reference); // Static adders and getters - m.def("register_point_cloud", &ps::registerPointCloud, + m.def("register_point_cloud", &ps::registerPointCloud, py::arg("name"), py::arg("values"), "Register a point cloud", py::return_value_policy::reference); - m.def("register_point_cloud2D", &ps::registerPointCloud2D, + m.def("register_point_cloud2D", &ps::registerPointCloud2D, py::arg("name"), py::arg("values"), "Register a point cloud", py::return_value_policy::reference); m.def("remove_point_cloud", &polyscope::removePointCloud, "Remove a point cloud by name"); m.def("get_point_cloud", &polyscope::getPointCloud, "Get a point cloud by name", py::return_value_policy::reference); diff --git a/src/cpp/surface_mesh.cpp b/src/cpp/surface_mesh.cpp index 79ae9d2..7eacdb6 100644 --- a/src/cpp/surface_mesh.cpp +++ b/src/cpp/surface_mesh.cpp @@ -7,6 +7,7 @@ #include "polyscope/polyscope.h" #include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" #include "utils.h" @@ -24,10 +25,12 @@ void bind_surface_mesh(py::module& m) { bindScalarQuantity(m, "SurfaceFaceScalarQuantity"); bindScalarQuantity(m, "SurfaceEdgeScalarQuantity"); bindScalarQuantity(m, "SurfaceHalfedgeScalarQuantity"); + bindScalarQuantity(m, "SurfaceTextureScalarQuantity"); // Color quantities bindColorQuantity(m, "SurfaceVertexColorQuantity"); bindColorQuantity(m, "SurfaceFaceColorQuantity"); + bindColorQuantity(m, "SurfaceTextureColorQuantity"); // Parameterization quantities py::class_(m, "SurfaceCornerParameterizationQuantity") @@ -36,33 +39,34 @@ void bind_surface_mesh(py::module& m) { .def("set_grid_colors", &ps::SurfaceCornerParameterizationQuantity::setGridColors, "Set grid colors") .def("set_checker_colors", &ps::SurfaceCornerParameterizationQuantity::setCheckerColors, "Set checker colors") .def("set_checker_size", &ps::SurfaceCornerParameterizationQuantity::setCheckerSize, "Set checker size") - .def("set_color_map", &ps::SurfaceCornerParameterizationQuantity::setColorMap, "Set color map"); + .def("set_color_map", &ps::SurfaceCornerParameterizationQuantity::setColorMap, "Set color map") + .def("set_island_labels", &ps::SurfaceCornerParameterizationQuantity::setIslandLabels) + .def("create_curve_network_from_seams", &ps::SurfaceCornerParameterizationQuantity::createCurveNetworkFromSeams, py::return_value_policy::reference); py::class_(m, "SurfaceVertexParameterizationQuantity") .def("set_enabled", &ps::SurfaceVertexParameterizationQuantity::setEnabled, "Set enabled") .def("set_style", &ps::SurfaceVertexParameterizationQuantity::setStyle, "Set style") .def("set_grid_colors", &ps::SurfaceVertexParameterizationQuantity::setGridColors, "Set grid colors") .def("set_checker_colors", &ps::SurfaceVertexParameterizationQuantity::setCheckerColors, "Set checker colors") .def("set_checker_size", &ps::SurfaceVertexParameterizationQuantity::setCheckerSize, "Set checker size") - .def("set_color_map", &ps::SurfaceVertexParameterizationQuantity::setColorMap, "Set color map"); + .def("set_color_map", &ps::SurfaceVertexParameterizationQuantity::setColorMap, "Set color map") + .def("set_island_labels", &ps::SurfaceVertexParameterizationQuantity::setIslandLabels) + .def("create_curve_network_from_seams", &ps::SurfaceVertexParameterizationQuantity::createCurveNetworkFromSeams, py::return_value_policy::reference); // Vector quantities bindVectorQuantity(m, "SurfaceVertexVectorQuantity"); bindVectorQuantity(m, "SurfaceFaceVectorQuantity"); - bindVectorQuantity(m, "SurfaceVertexIntrinsicVectorQuantity") - .def("set_ribbon_enabled", &ps::SurfaceVertexIntrinsicVectorQuantity::setEnabled, "Set ribbon enabled"); - bindVectorQuantity(m, "SurfaceFaceIntrinsicVectorQuantity") - .def("set_ribbon_enabled", &ps::SurfaceFaceIntrinsicVectorQuantity::setEnabled, "Set ribbon enabled"); - bindVectorQuantity(m, "SurfaceOneFormIntrinsicVectorQuantity") - .def("set_ribbon_enabled", &ps::SurfaceOneFormIntrinsicVectorQuantity::setEnabled, "Set ribbon enabled"); + bindVectorQuantity(m, "SurfaceVertexTangentVectorQuantity"); + bindVectorQuantity(m, "SurfaceFaceTangentVectorQuantity"); + bindVectorQuantity(m, "SurfaceOneFormTangentVectorQuantity"); // == Main class bindStructure(m, "SurfaceMesh") // basics - .def("update_vertex_positions", &ps::SurfaceMesh::updateVertexPositions, + .def("update_vertex_positions", &ps::SurfaceMesh::updateVertexPositions, "Update vertex positions") - .def("update_vertex_positions2D", &ps::SurfaceMesh::updateVertexPositions2D, + .def("update_vertex_positions2D", &ps::SurfaceMesh::updateVertexPositions2D, "Update vertex positions") .def("n_vertices", &ps::SurfaceMesh::nVertices, "# vertices") .def("n_faces", &ps::SurfaceMesh::nFaces, "# faces") @@ -79,6 +83,8 @@ void bind_surface_mesh(py::module& m) { .def("get_edge_width", &ps::SurfaceMesh::getEdgeWidth, "Get edge width") .def("set_smooth_shade", &ps::SurfaceMesh::setSmoothShade, "Set smooth shading") .def("get_smooth_shade", &ps::SurfaceMesh::isSmoothShade, "Get if smooth shading is enabled") + .def("set_shade_style", &ps::SurfaceMesh::setShadeStyle, "Set shading") + .def("get_shade_style", &ps::SurfaceMesh::getShadeStyle, "Get shading") .def("set_material", &ps::SurfaceMesh::setMaterial, "Set material") .def("get_material", &ps::SurfaceMesh::getMaterial, "Get material") .def("set_back_face_policy", &ps::SurfaceMesh::setBackFacePolicy, "Set back face policy") @@ -86,89 +92,90 @@ void bind_surface_mesh(py::module& m) { .def("set_back_face_color", &ps::SurfaceMesh::setBackFaceColor, "Set back face color") .def("get_back_face_color", &ps::SurfaceMesh::getBackFaceColor, "Get back face color") - // slice planes - .def("set_ignore_slice_plane", &ps::SurfaceMesh::setIgnoreSlicePlane, "Set ignore slice plane") - .def("get_ignore_slice_plane", &ps::SurfaceMesh::getIgnoreSlicePlane, "Get ignore slice plane") - .def("set_cull_whole_elements", &ps::SurfaceMesh::setCullWholeElements, "Set cull whole elements") - .def("get_cull_whole_elements", &ps::SurfaceMesh::getCullWholeElements, "Get cull whole elements") - - // permutations & bases - .def("set_vertex_permutation", &ps::SurfaceMesh::setVertexPermutation, "Set vertex permutation") - .def("set_face_permutation", &ps::SurfaceMesh::setFacePermutation, "Set face permutation") .def("set_edge_permutation", &ps::SurfaceMesh::setEdgePermutation, "Set edge permutation") .def("set_halfedge_permutation", &ps::SurfaceMesh::setHalfedgePermutation, "Set halfedge permutation") .def("set_corner_permutation", &ps::SurfaceMesh::setCornerPermutation, "Set corner permutation") - .def("set_vertex_tangent_basisX", &ps::SurfaceMesh::setVertexTangentBasisX, - "Set vertex tangent bases") - .def("set_vertex_tangent_basisX2D", &ps::SurfaceMesh::setVertexTangentBasisX2D, - "Set vertex tangent bases") - .def("set_face_tangent_basisX", &ps::SurfaceMesh::setFaceTangentBasisX, "Set face tangent bases") - .def("set_face_tangent_basisX2D", &ps::SurfaceMesh::setFaceTangentBasisX2D, - "Set face tangent bases") + + .def("mark_edges_as_used", &ps::SurfaceMesh::markEdgesAsUsed) + .def("mark_halfedges_as_used", &ps::SurfaceMesh::markHalfedgesAsUsed) + .def("mark_corners_as_used", &ps::SurfaceMesh::markCornersAsUsed) // = quantities // Scalars - .def("add_vertex_scalar_quantity", &ps::SurfaceMesh::addVertexScalarQuantity, + .def("add_vertex_scalar_quantity", &ps::SurfaceMesh::addVertexScalarQuantity, "Add a scalar function at vertices", py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, py::return_value_policy::reference) - .def("add_face_scalar_quantity", &ps::SurfaceMesh::addFaceScalarQuantity, + .def("add_face_scalar_quantity", &ps::SurfaceMesh::addFaceScalarQuantity, "Add a scalar function at faces", py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, py::return_value_policy::reference) - .def("add_edge_scalar_quantity", &ps::SurfaceMesh::addEdgeScalarQuantity, + .def("add_edge_scalar_quantity", &ps::SurfaceMesh::addEdgeScalarQuantity, "Add a scalar function at edges", py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, py::return_value_policy::reference) - .def("add_halfedge_scalar_quantity", &ps::SurfaceMesh::addHalfedgeScalarQuantity, + .def("add_halfedge_scalar_quantity", &ps::SurfaceMesh::addHalfedgeScalarQuantity, "Add a scalar function at halfedges", py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, py::return_value_policy::reference) + .def("add_texture_scalar_quantity", + overload_cast_()(&ps::SurfaceMesh::addTextureScalarQuantity), + "Add a scalar function from a texture map", py::arg("name"), py::arg("param_name"), py::arg("dimX"), + py::arg("dimY"), py::arg("values"), py::arg("image_origin"), py::arg("data_type") = ps::DataType::STANDARD, + py::return_value_policy::reference) // Colors - .def("add_vertex_color_quantity", &ps::SurfaceMesh::addVertexColorQuantity, + .def("add_vertex_color_quantity", &ps::SurfaceMesh::addVertexColorQuantity, "Add a color value at vertices", py::return_value_policy::reference) - .def("add_face_color_quantity", &ps::SurfaceMesh::addFaceColorQuantity, + .def("add_face_color_quantity", &ps::SurfaceMesh::addFaceColorQuantity, "Add a color value at faces", py::return_value_policy::reference) + .def("add_texture_color_quantity", + overload_cast_()( + &ps::SurfaceMesh::addTextureColorQuantity), + "Add a color function from a texture map", py::arg("name"), py::arg("param_name"), py::arg("dimX"), + py::arg("dimY"), py::arg("colors"), py::arg("image_origin"), py::return_value_policy::reference) // Distance - .def("add_vertex_distance_quantity", &ps::SurfaceMesh::addVertexDistanceQuantity, + .def("add_vertex_distance_quantity", &ps::SurfaceMesh::addVertexDistanceQuantity, "Add a distance function at vertices", py::return_value_policy::reference) - .def("add_vertex_signed_distance_quantity", &ps::SurfaceMesh::addVertexSignedDistanceQuantity, + .def("add_vertex_signed_distance_quantity", &ps::SurfaceMesh::addVertexSignedDistanceQuantity, "Add a signed distance function at vertices", py::return_value_policy::reference) // Parameterization - .def("add_corner_parameterization_quantity", &ps::SurfaceMesh::addParameterizationQuantity, + .def("add_corner_parameterization_quantity", &ps::SurfaceMesh::addParameterizationQuantity, "Add a parameterization at corners", py::return_value_policy::reference) - .def("add_vertex_parameterization_quantity", &ps::SurfaceMesh::addVertexParameterizationQuantity, + .def("add_vertex_parameterization_quantity", &ps::SurfaceMesh::addVertexParameterizationQuantity, "Add a parameterization at vertices", py::return_value_policy::reference) // Vector - .def("add_vertex_vector_quantity", &ps::SurfaceMesh::addVertexVectorQuantity, + .def("add_vertex_vector_quantity", &ps::SurfaceMesh::addVertexVectorQuantity, "Add a vertex vector quantity", py::return_value_policy::reference) - .def("add_face_vector_quantity", &ps::SurfaceMesh::addFaceVectorQuantity, + .def("add_face_vector_quantity", &ps::SurfaceMesh::addFaceVectorQuantity, "Add a face vector quantity", py::return_value_policy::reference) - .def("add_vertex_vector_quantity2D", &ps::SurfaceMesh::addVertexVectorQuantity2D, + .def("add_vertex_vector_quantity2D", &ps::SurfaceMesh::addVertexVectorQuantity2D, "Add a vertex 2D vector quantity", py::return_value_policy::reference) - .def("add_face_vector_quantity2D", &ps::SurfaceMesh::addFaceVectorQuantity2D, + .def("add_face_vector_quantity2D", &ps::SurfaceMesh::addFaceVectorQuantity2D, "Add a face 2D vector quantity", py::return_value_policy::reference) - .def("add_vertex_intrinsic_vector_quantity", &ps::SurfaceMesh::addVertexIntrinsicVectorQuantity, - "Add a vertex intrinsic vector quantity", py::return_value_policy::reference) - .def("add_face_intrinsic_vector_quantity", &ps::SurfaceMesh::addFaceIntrinsicVectorQuantity, - "Add a face intrinsic vector quantity", py::return_value_policy::reference) - .def("add_one_form_intrinsic_vector_quantity", - &ps::SurfaceMesh::addOneFormIntrinsicVectorQuantity>, - "Add a one form intrinsic vector quantity", py::return_value_policy::reference); + .def("add_vertex_tangent_vector_quantity", + &ps::SurfaceMesh::addVertexTangentVectorQuantity, + "Add a vertex tangent vector quantity", py::return_value_policy::reference) + .def("add_face_tangent_vector_quantity", + &ps::SurfaceMesh::addFaceTangentVectorQuantity, + "Add a face tangent vector quantity", py::return_value_policy::reference) + .def("add_one_form_tangent_vector_quantity", + &ps::SurfaceMesh::addOneFormTangentVectorQuantity>, + "Add a one form tangent vector quantity", py::return_value_policy::reference); // Static adders and getters - m.def("register_surface_mesh", &ps::registerSurfaceMesh, py::arg("name"), + m.def("register_surface_mesh", &ps::registerSurfaceMesh, py::arg("name"), py::arg("vertices"), py::arg("faces"), "Register a surface mesh", py::return_value_policy::reference); - m.def("register_surface_mesh2D", &ps::registerSurfaceMesh2D, py::arg("name"), + m.def("register_surface_mesh2D", &ps::registerSurfaceMesh2D, py::arg("name"), py::arg("vertices"), py::arg("faces"), "Register a surface mesh", py::return_value_policy::reference); - m.def("register_surface_mesh_list", &ps::registerSurfaceMesh>>, + m.def("register_surface_mesh_list", &ps::registerSurfaceMesh>>, py::arg("name"), py::arg("vertices"), py::arg("faces"), "Register a surface mesh from a nested list", py::return_value_policy::reference); - m.def("register_surface_mesh_list2D", &ps::registerSurfaceMesh2D>>, + m.def("register_surface_mesh_list2D", &ps::registerSurfaceMesh2D>>, py::arg("name"), py::arg("vertices"), py::arg("faces"), "Register a surface mesh from a nested list", py::return_value_policy::reference); diff --git a/src/cpp/utils.h b/src/cpp/utils.h index cf6458c..7e8da76 100644 --- a/src/cpp/utils.h +++ b/src/cpp/utils.h @@ -8,7 +8,14 @@ #include "Eigen/Dense" +#include "polyscope/image_quantity.h" + namespace py = pybind11; +namespace ps = polyscope; + +// For overloaded functions, with C++11 compiler only +template +using overload_cast_ = pybind11::detail::overload_cast_impl; // Some conversion helpers template @@ -48,22 +55,73 @@ inline Eigen::Matrix glm2eigen(const glm::vec& vec_glm) { return vec_eigen; } +template +void def_get_managed_buffer(C& c, std::string postfix) { + c.def(("get_buffer_" + postfix).c_str(), + [](StructureT& s, std::string buffer_name) -> ps::render::ManagedBuffer& { + return s.template getManagedBuffer(buffer_name); + }, + "get managed buffer", py::return_value_policy::reference); +} + +template +void def_get_quantity_managed_buffer(C& c, std::string postfix) { + c.def(("get_quantity_buffer_" + postfix).c_str(), + [](StructureT& s, std::string quantity_name, std::string buffer_name) -> ps::render::ManagedBuffer& { + ps::Quantity* qPtr = s.getQuantity(quantity_name); + if (qPtr) { + return qPtr->template getManagedBuffer(buffer_name); + } + ps::FloatingQuantity* fqPtr = s.getFloatingQuantity(quantity_name); + if (fqPtr) { + return fqPtr->template getManagedBuffer(buffer_name); + } + ps::exception("structure " + s.name + " has no quantity " + quantity_name); + + // this line should be unreachable, it is here to convince the compiler that we don't need a return value + // here, to silence warnings + throw std::logic_error("structure has no such quantity"); + }, + "get quantity managed buffer", py::return_value_policy::reference); +} + + +template +void def_all_managed_buffer_funcs(C& c, ps::ManagedBufferType t) { + def_get_managed_buffer(c, ps::typeName(t)); + def_get_quantity_managed_buffer(c, ps::typeName(t)); +} + + // Add common bindings for structures template py::class_ bindStructure(py::module& m, std::string name) { - return py::class_(m, name.c_str()) - // structure basics - .def("remove", &StructureT::remove, "Remove the structure") + py::class_ s(m, name.c_str()); + + // structure basics + s.def("remove", &StructureT::remove, "Remove the structure") + .def("get_name", [](StructureT& s) { return s.name; }, "Ge the name") + .def("get_unique_prefix", &StructureT::uniquePrefix, "Get unique prefix") .def("set_enabled", &StructureT::setEnabled, "Enable the structure") .def("enable_isolate", &StructureT::enableIsolate, "Enable the structure, disable all of same type") .def("is_enabled", &StructureT::isEnabled, "Check if the structure is enabled") .def("set_transparency", &StructureT::setTransparency, "Set transparency alpha") .def("get_transparency", &StructureT::getTransparency, "Get transparency alpha") + + // group things + .def("add_to_group", overload_cast_()(&StructureT::addToGroup) , "Add to group") + + // slice plane things + .def("set_ignore_slice_plane", &StructureT::setIgnoreSlicePlane, "Set ignore slice plane") + .def("get_ignore_slice_plane", &StructureT::getIgnoreSlicePlane, "Get ignore slice plane") + .def("set_cull_whole_elements", &StructureT::setCullWholeElements, "Set cull whole elements") + .def("get_cull_whole_elements", &StructureT::getCullWholeElements, "Get cull whole elememts") // quantites .def("remove_all_quantities", &StructureT::removeAllQuantities, "Remove all quantities") - .def("remove_quantity", &StructureT::removeQuantity, "Remove a quantity") + .def("remove_quantity", &StructureT::removeQuantity, py::arg("name"), py::arg("errorIfAbsent") = false, + "Remove a quantity") // transform management // clang-format off @@ -74,45 +132,115 @@ py::class_ bindStructure(py::module& m, std::string name) { .def("set_position", [](StructureT& s, Eigen::Vector3f T) { s.setPosition(eigen2glm(T)); }, "set the translation component of the transform to the given position") .def("translate", [](StructureT& s, Eigen::Vector3f T) { s.translate(eigen2glm(T)); }, "apply the given translation to the shape, updating its position") .def("get_transform", [](StructureT& s) { return glm2eigen(s.getTransform()); }, "get the current 4x4 transform matrix") - .def("get_position", [](StructureT& s) { return glm2eigen(s.getPosition()); }, "get the position of the shape origin after transform"); + .def("get_position", [](StructureT& s) { return glm2eigen(s.getPosition()); }, "get the position of the shape origin after transform") + + // floating quantites + .def("add_scalar_image_quantity", &StructureT::template addScalarImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("values"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::arg("type")=ps::DataType::STANDARD, py::return_value_policy::reference) + .def("add_color_image_quantity", &StructureT::template addColorImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("values_rgb"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::return_value_policy::reference) + .def("add_color_alpha_image_quantity", &StructureT::template addColorAlphaImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("values_rgba"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::return_value_policy::reference) + .def("add_depth_render_image_quantity", &StructureT::template addDepthRenderImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("depthData"), py::arg("normalData"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::return_value_policy::reference) + .def("add_color_render_image_quantity", &StructureT::template addColorRenderImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("depthData"), py::arg("normalData"), py::arg("colorData"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::return_value_policy::reference) + .def("add_scalar_render_image_quantity", &StructureT::template addScalarRenderImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("depthData"), py::arg("normalData"), py::arg("scalarData"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::arg("type")=ps::DataType::STANDARD, py::return_value_policy::reference) + .def("add_raw_color_render_image_quantity", &StructureT::template addRawColorRenderImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("depthData"), py::arg("colorData"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::return_value_policy::reference) + .def("add_raw_color_alpha_render_image_quantity", &StructureT::template addRawColorAlphaRenderImageQuantity, py::arg("name"), py::arg("dimX"), py::arg("dimY"), py::arg("depthData"), py::arg("colorData"), py::arg("imageOrigin")=ps::ImageOrigin::UpperLeft, py::return_value_policy::reference) + + ; + + // managed buffer things + def_all_managed_buffer_funcs (s, ps::ManagedBufferType::Float); + def_all_managed_buffer_funcs(s, ps::ManagedBufferType::Double); + + def_all_managed_buffer_funcs(s, ps::ManagedBufferType::Vec2); + def_all_managed_buffer_funcs(s, ps::ManagedBufferType::Vec3); + def_all_managed_buffer_funcs(s, ps::ManagedBufferType::Vec4); + + def_all_managed_buffer_funcs>(s, ps::ManagedBufferType::Arr2Vec3); + def_all_managed_buffer_funcs>(s, ps::ManagedBufferType::Arr3Vec3); + def_all_managed_buffer_funcs>(s, ps::ManagedBufferType::Arr4Vec3); + + def_all_managed_buffer_funcs (s, ps::ManagedBufferType::UInt32); + def_all_managed_buffer_funcs (s, ps::ManagedBufferType::Int32); + + def_all_managed_buffer_funcs(s, ps::ManagedBufferType::UVec2); + def_all_managed_buffer_funcs(s, ps::ManagedBufferType::UVec3); + def_all_managed_buffer_funcs(s, ps::ManagedBufferType::UVec4); + + s.def("has_buffer_type", &StructureT::hasManagedBufferType, "has managed buffer type"); + s.def("has_quantity_buffer_type", [](StructureT& s, std::string quantity_name, std::string buffer_name) { + ps::Quantity* qPtr = s.getQuantity(quantity_name); + if (qPtr) { + return qPtr->hasManagedBufferType(buffer_name); + } + ps::FloatingQuantity* fqPtr = s.getFloatingQuantity(quantity_name); + if (fqPtr) { + return fqPtr->hasManagedBufferType(buffer_name); + } + return std::make_tuple(false, ps::ManagedBufferType::Float); + }, "has quantity managed buffer type"); + + // clang-format on + return s; +} + +// Common bindings for quantities that do not fall in to a more specific quantity below +template +py::class_ bindQuantity(py::module& m, std::string name) { + return py::class_(m, name.c_str()).def("set_enabled", &Q::setEnabled, "Set enabled"); } // Add common bindings for all scalar quantities template py::class_ bindScalarQuantity(py::module& m, std::string name) { - return py::class_(m, name.c_str()) - .def("set_enabled", &ScalarQ::setEnabled, "Set enabled") + return bindQuantity(m, name.c_str()) .def("set_color_map", &ScalarQ::setColorMap, "Set color map") .def("set_map_range", &ScalarQ::setMapRange, "Set map range") .def("set_isoline_width", &ScalarQ::setIsolineWidth, "Set isoline width"); } -template +template py::class_ bindVMVScalarQuantity(py::module& m, std::string name) { - return py::class_(m, name.c_str()) - .def("set_enabled", &VolumeMeshVertexScalarQuantity::setEnabled, "Set enabled") - .def("set_color_map", &VolumeMeshVertexScalarQuantity::setColorMap, "Set color map") - .def("set_map_range", &VolumeMeshVertexScalarQuantity::setMapRange, "Set map range") - .def("set_isoline_width", &VolumeMeshVertexScalarQuantity::setIsolineWidth, "Set isoline width") - .def("set_level_set_enable", &VolumeMeshVertexScalarQuantity::setEnabledLevelSet, "Set level set rendering enabled") - .def("set_level_set_value", &VolumeMeshVertexScalarQuantity::setLevelSetValue, "Set level set value") - .def("set_level_set_visible_quantity", &VolumeMeshVertexScalarQuantity::setLevelSetVisibleQuantity, "Set quantity to show on level set"); + return bindQuantity(m, name.c_str()) + .def("set_color_map", &VolumeMeshVertexScalarQuantity::setColorMap, "Set color map") + .def("set_map_range", &VolumeMeshVertexScalarQuantity::setMapRange, "Set map range") + .def("set_isoline_width", &VolumeMeshVertexScalarQuantity::setIsolineWidth, "Set isoline width") + .def("set_level_set_enable", &VolumeMeshVertexScalarQuantity::setEnabledLevelSet, + "Set level set rendering enabled") + .def("set_level_set_value", &VolumeMeshVertexScalarQuantity::setLevelSetValue, "Set level set value") + .def("set_level_set_visible_quantity", &VolumeMeshVertexScalarQuantity::setLevelSetVisibleQuantity, + "Set quantity to show on level set"); } // Add common bindings for all color quantities template py::class_ bindColorQuantity(py::module& m, std::string name) { - return py::class_(m, name.c_str()).def("set_enabled", &ColorQ::setEnabled, "Set enabled"); + return bindQuantity(m, name.c_str()); } // Add common bindings for all vector quantities template py::class_ bindVectorQuantity(py::module& m, std::string name) { - return py::class_(m, name.c_str()) - .def("set_enabled", &VectorQ::setEnabled, "Set enabled") + return bindQuantity(m, name.c_str()) .def("set_length", &VectorQ::setVectorLengthScale, "Set length") .def("set_radius", &VectorQ::setVectorRadius, "Set radius") .def("set_color", &VectorQ::setVectorColor, "Set color"); } + +// Add common image options +// Note: unlike the others above, this adds methods to an existing quantity rather than binding a new one. +template +void addImageQuantityBindings(py::class_& imageQ) { + + imageQ.def("set_show_fullscreen", &ImageQ::setShowFullscreen); + imageQ.def("get_show_fullscreen", &ImageQ::getShowFullscreen); + + imageQ.def("set_show_in_imgui_window", &ImageQ::setShowInImGuiWindow); + imageQ.def("get_show_in_imgui_window", &ImageQ::getShowInImGuiWindow); + + imageQ.def("set_show_in_camera_billboard", &ImageQ::setShowInCameraBillboard); + imageQ.def("get_show_in_camera_billboard", &ImageQ::getShowInCameraBillboard); + + imageQ.def("set_transparency", &ImageQ::setTransparency); + imageQ.def("get_transparency", &ImageQ::getTransparency); +} diff --git a/src/cpp/volume_grid.cpp b/src/cpp/volume_grid.cpp new file mode 100644 index 0000000..683bc48 --- /dev/null +++ b/src/cpp/volume_grid.cpp @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include + +#include "Eigen/Dense" + +#include "polyscope/polyscope.h" +#include "polyscope/volume_grid.h" + +#include "utils.h" + +namespace py = pybind11; +namespace ps = polyscope; + + +void bind_volume_grid(py::module& m) { + + // == Helper quantity classes + + // Scalar quantities + bindScalarQuantity(m, "VolumeGridNodeScalarQuantity") + .def("set_gridcube_viz_enabled", &ps::VolumeGridNodeScalarQuantity::setGridcubeVizEnabled) + .def("set_isosurface_viz_enabled", &ps::VolumeGridNodeScalarQuantity::setIsosurfaceVizEnabled) + .def("set_isosurface_level", &ps::VolumeGridNodeScalarQuantity::setIsosurfaceLevel) + .def("set_isosurface_color", &ps::VolumeGridNodeScalarQuantity::setIsosurfaceColor) + .def("set_slice_planes_affect_isosurface", &ps::VolumeGridNodeScalarQuantity::setSlicePlanesAffectIsosurface) + .def("register_isosurface_as_mesh_with_name", + [](ps::VolumeGridNodeScalarQuantity& x, std::string name) { return x.registerIsosurfaceAsMesh(name); }, + py::return_value_policy::reference) + ; + + bindScalarQuantity(m, "VolumeGridCellScalarQuantity") + .def("set_gridcube_viz_enabled", &ps::VolumeGridCellScalarQuantity::setGridcubeVizEnabled) + ; + + // == Main class + bindStructure(m, "VolumeGrid") + + // basics + .def("n_nodes", &ps::VolumeGrid::nNodes) + .def("n_cells", &ps::VolumeGrid::nCells) + .def("grid_spacing", &ps::VolumeGrid::gridSpacing) + .def("get_grid_node_dim", &ps::VolumeGrid::getGridNodeDim) + .def("get_grid_cell_dim", &ps::VolumeGrid::getGridCellDim) + .def("get_bound_min", [](ps::VolumeGrid& x) { return glm2eigen(x.getBoundMin()); }) + .def("get_bound_max", [](ps::VolumeGrid& x) { return glm2eigen(x.getBoundMax()); }) + + .def("mark_nodes_as_used", &ps::VolumeGrid::markNodesAsUsed) + .def("mark_cells_as_used", &ps::VolumeGrid::markCellsAsUsed) + + // options + .def("set_color", &ps::VolumeGrid::setColor) + .def("get_color", &ps::VolumeGrid::getColor) + .def("set_edge_color", &ps::VolumeGrid::setEdgeColor) + .def("get_edge_color", &ps::VolumeGrid::getEdgeColor) + .def("set_material", &ps::VolumeGrid::setMaterial, "Set material") + .def("get_material", &ps::VolumeGrid::getMaterial, "Get material") + .def("set_edge_width", &ps::VolumeGrid::setEdgeWidth, "Set edge width") + .def("get_edge_width", &ps::VolumeGrid::getEdgeWidth, "Get edge width") + .def("set_cube_size_factor", &ps::VolumeGrid::setCubeSizeFactor, "Set cube size factor") + .def("get_cube_size_factor", &ps::VolumeGrid::getCubeSizeFactor, "Get cube size factor") + + + // = quantities + + // Scalars + .def("add_node_scalar_quantity", &ps::VolumeGrid::addNodeScalarQuantity, + py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, + py::return_value_policy::reference) + + .def("add_cell_scalar_quantity", &ps::VolumeGrid::addCellScalarQuantity, + py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, + py::return_value_policy::reference) + + // add from a callable lambda + .def("add_node_scalar_quantity_from_callable", []( + ps::VolumeGrid& grid, + std::string name, + const std::function)>& func, + ps::DataType data_type) { + + // Polyscope's API uses raw buffer pointers, but we use Eigen mats for pybind11. + // Create a wrapper function that goes to/from the Eigen mats + auto wrapped_func = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map mapped_result(result_ptr, size); + mapped_result = func(mapped_pos); + }; + + return grid.addNodeScalarQuantityFromBatchCallable(name, wrapped_func, data_type); + }, + py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, + py::return_value_policy::reference) + + .def("add_cell_scalar_quantity_from_callable", []( + ps::VolumeGrid& grid, + std::string name, + const std::function)>& func, + ps::DataType data_type) { + + // Polyscope's API uses raw buffer pointers, but we use Eigen mats for pybind11. + // Create a wrapper function that goes to/from the Eigen mats + auto wrapped_func = [&](const float* pos_ptr, float* result_ptr, uint64_t size) { + Eigen::Map> mapped_pos(pos_ptr, size, 3); + Eigen::Map mapped_result(result_ptr, size); + mapped_result = func(mapped_pos); + }; + + return grid.addNodeScalarQuantityFromBatchCallable(name, wrapped_func, data_type); + }, + py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, + py::return_value_policy::reference) + + // Colors + + // Vectors + + ; + + // Static adders and getters + m.def("register_volume_grid", overload_cast_()(&ps::registerVolumeGrid), py::arg("name"), + py::arg("gridNodeDim"), py::arg("boundMin"), py::arg("boundMax"), py::return_value_policy::reference); + + m.def("remove_volume_grid", &polyscope::removeVolumeGrid); + m.def("get_volume_grid", &polyscope::getVolumeGrid, py::return_value_policy::reference); + m.def("has_volume_grid", &polyscope::hasVolumeGrid); +} diff --git a/src/cpp/volume_mesh.cpp b/src/cpp/volume_mesh.cpp index c088c3c..ada5d71 100644 --- a/src/cpp/volume_mesh.cpp +++ b/src/cpp/volume_mesh.cpp @@ -34,7 +34,7 @@ void bind_volume_mesh(py::module& m) { bindStructure(m, "VolumeMesh") // basics - .def("update_vertex_positions", &ps::VolumeMesh::updateVertexPositions, + .def("update_vertex_positions", &ps::VolumeMesh::updateVertexPositions, "Update vertex positions") .def("n_vertices", &ps::VolumeMesh::nVertices, "# vertices") .def("n_faces", &ps::VolumeMesh::nFaces, "# faces") @@ -52,48 +52,42 @@ void bind_volume_mesh(py::module& m) { .def("set_material", &ps::VolumeMesh::setMaterial, "Set material") .def("get_material", &ps::VolumeMesh::getMaterial, "Get material") - // slice planes - .def("set_ignore_slice_plane", &ps::VolumeMesh::setIgnoreSlicePlane, "Set ignore slice plane") - .def("get_ignore_slice_plane", &ps::VolumeMesh::getIgnoreSlicePlane, "Get ignore slice plane") - .def("set_cull_whole_elements", &ps::VolumeMesh::setCullWholeElements, "Set cull whole elements") - .def("get_cull_whole_elements", &ps::VolumeMesh::getCullWholeElements, "Get cull whole elements") - // = quantities // Scalars - .def("add_vertex_scalar_quantity", &ps::VolumeMesh::addVertexScalarQuantity, + .def("add_vertex_scalar_quantity", &ps::VolumeMesh::addVertexScalarQuantity, "Add a scalar function at vertices", py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, py::return_value_policy::reference) - .def("add_cell_scalar_quantity", &ps::VolumeMesh::addCellScalarQuantity, + .def("add_cell_scalar_quantity", &ps::VolumeMesh::addCellScalarQuantity, "Add a scalar function at cells", py::arg("name"), py::arg("values"), py::arg("data_type") = ps::DataType::STANDARD, py::return_value_policy::reference) // Colors - .def("add_vertex_color_quantity", &ps::VolumeMesh::addVertexColorQuantity, + .def("add_vertex_color_quantity", &ps::VolumeMesh::addVertexColorQuantity, "Add a color value at vertices", py::return_value_policy::reference) - .def("add_cell_color_quantity", &ps::VolumeMesh::addCellColorQuantity, + .def("add_cell_color_quantity", &ps::VolumeMesh::addCellColorQuantity, "Add a color value at cells", py::return_value_policy::reference) // Vector - .def("add_vertex_vector_quantity", &ps::VolumeMesh::addVertexVectorQuantity, + .def("add_vertex_vector_quantity", &ps::VolumeMesh::addVertexVectorQuantity, "Add a vertex vector quantity", py::return_value_policy::reference) - .def("add_cell_vector_quantity", &ps::VolumeMesh::addCellVectorQuantity, + .def("add_cell_vector_quantity", &ps::VolumeMesh::addCellVectorQuantity, "Add a cell vector quantity", py::return_value_policy::reference); // Static adders and getters - m.def("register_tet_mesh", &ps::registerTetMesh, py::arg("name"), + m.def("register_tet_mesh", &ps::registerTetMesh, py::arg("name"), py::arg("vertices"), py::arg("tets"), "Register a volume mesh of tet cells", py::return_value_policy::reference); - m.def("register_hex_mesh", &ps::registerHexMesh, py::arg("name"), + m.def("register_hex_mesh", &ps::registerHexMesh, py::arg("name"), py::arg("vertices"), py::arg("hexes"), "Register a volume mesh of hex cells", py::return_value_policy::reference); - m.def("register_volume_mesh", &ps::registerVolumeMesh, py::arg("name"), + m.def("register_volume_mesh", &ps::registerVolumeMesh, py::arg("name"), py::arg("vertices"), py::arg("cells"), "Register a volume mesh with a mix of element types", py::return_value_policy::reference); - m.def("register_tet_hex_mesh", &ps::registerTetHexMesh, + m.def("register_tet_hex_mesh", &ps::registerTetHexMesh, py::arg("name"), py::arg("vertices"), py::arg("tets"), py::arg("hexes"), "Register a volume mesh with lists of tet and hex elements", py::return_value_policy::reference); diff --git a/src/polyscope/__init__.py b/src/polyscope/__init__.py index 26a2fc5..9b606cb 100644 --- a/src/polyscope/__init__.py +++ b/src/polyscope/__init__.py @@ -1,5 +1,15 @@ from polyscope.core import * + +from polyscope.structure import * +from polyscope.floating_quantities import * +from polyscope.managed_buffer import * +from polyscope.implicit_helpers import * +from polyscope.device_interop import * + from polyscope.surface_mesh import * from polyscope.point_cloud import * from polyscope.curve_network import * from polyscope.volume_mesh import * +from polyscope.volume_grid import * +from polyscope.camera_view import * +from polyscope.global_floating_quantity_structure import * diff --git a/src/polyscope/camera_view.py b/src/polyscope/camera_view.py new file mode 100644 index 0000000..d57e6ce --- /dev/null +++ b/src/polyscope/camera_view.py @@ -0,0 +1,104 @@ +import polyscope_bindings as psb + +from polyscope.core import glm3, CameraParameters +from polyscope.structure import Structure + +import numpy as np + +class CameraView(Structure): + + # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope + + # End users should not call this constrctor, use register_camera_view instead + def __init__(self, name=None, camera_parameters=None, instance=None): + + super().__init__() + + if instance is not None: + # Wrap an existing instance + self.bound_instance = instance + + else: + # Create a new instance + self.bound_instance = psb.register_camera_view(name, camera_parameters.instance) + + + # Update + def update_camera_parameters(self, camera_parameters): + self.bound_instance.update_camera_parameters(camera_parameters.instance) + + + ## Camera things + + def set_view_to_this_camera(self, with_flight=False): + self.bound_instance.set_view_to_this_camera(with_flight) + + def get_camera_parameters(self): + return CameraParameters(instance=self.bound_instance.get_camera_parameters()) + + ## Options + + # Widget color + def set_widget_color(self, val): + self.bound_instance.set_widget_color(glm3(val)) + def get_widget_color(self): + return self.bound_instance.get_widget_color().as_tuple() + + # Widget thickness + def set_widget_thickness(self, val): + self.bound_instance.set_widget_thickness(float(val)) + def get_widget_thickness(self): + return self.bound_instance.get_widget_thickness() + + # Widget focal length + def set_widget_focal_length(self, val, relative=True): + self.bound_instance.set_widget_focal_length(float(val), relative) + def get_widget_focal_length(self): + return self.bound_instance.get_widget_focal_length() + + + ## Quantities + + +def register_camera_view(name, camera_parameters, + enabled=None, transparency=None, + widget_color=None, widget_thickness=None, widget_focal_length=None, + ): + """Register a new camera view""" + if not psb.is_initialized(): + raise RuntimeError("Polyscope has not been initialized") + + p = CameraView(name, camera_parameters) + + # == Apply options + if enabled is not None: + p.set_enabled(enabled) + if transparency is not None: + p.set_transparency(transparency) + if widget_color is not None: + p.set_widget_color(widget_color) + if widget_thickness is not None: + p.set_widget_thickness(widget_thickness) + if widget_focal_length is not None: + p.set_widget_focal_length(widget_focal_length) + + return p + +def remove_camera_view(name, error_if_absent=True): + """Remove a camera view by name""" + psb.remove_camera_view(name, error_if_absent) + +def get_camera_view(name): + """Get camera view by name""" + if not has_camera_view(name): + raise ValueError("no camera view with name " + str(name)) + + raw_instance = psb.get_camera_view(name) + + # Wrap the instance + return CameraView(instance=raw_instance) + +def has_camera_view(name): + """Check if a camera view exists by name""" + return psb.has_camera_view(name) + diff --git a/src/polyscope/common.py b/src/polyscope/common.py new file mode 100644 index 0000000..931268e --- /dev/null +++ b/src/polyscope/common.py @@ -0,0 +1,205 @@ +import polyscope_bindings as psb + +from polyscope.core import str_to_datatype, str_to_vectortype, glm3, str_to_point_render_mode, point_render_mode_to_str, str_to_param_viz_style + + +def check_is_scalar_image(values): + if len(values.shape) != 2: + raise ValueError(f"'values' should be a (height,width) array. Shape is {values.shape}.") + +def check_is_image3(values): + if len(values.shape) != 3 or values.shape[2] != 3: + raise ValueError(f"'values' should be a (height,width,3) array. Shape is {values.shape}.") + +def check_is_image4(values): + if len(values.shape) != 3 or values.shape[2] != 4: + raise ValueError(f"'values' should be a (height,width,4) array. Shape is {values.shape}.") + +def check_image_dims_compatible(images): + + dimX = None + dimY = None + + for img in images: + + if dimX is None: + dimX = img.shape[0] + dimY = img.shape[1] + else: + new_dimX = img.shape[0] + new_dimY = img.shape[1] + + if (dimX, dimY) != (new_dimX, new_dimY): + raise ValueError(f"image inputs must have same resolution. One is {(dimX,dimY)} but another is {(new_dimX, new_dimY)}.") + +def check_and_pop_arg(d, argname): + if argname in d: + return d.pop(argname) + return None + +# Process args, removing them from the dict if they are present +def process_quantity_args(structure, quantity, quantity_args): + + val = check_and_pop_arg(quantity_args, 'enabled') + if val is not None: + quantity.set_enabled(val) + +# Process args, removing them from the dict if they are present +def process_color_args(structure, quantity, color_args): + + val = check_and_pop_arg(color_args, 'is_premultiplied') + if val is not None: + quantity.set_is_premultiplied(val) + + +# Process args, removing them from the dict if they are present +def process_scalar_args(structure, quantity, scalar_args): + + val = check_and_pop_arg(scalar_args, 'vminmax') + if val is not None: + quantity.set_map_range(val) + + val = check_and_pop_arg(scalar_args, 'cmap') + if val is not None: + quantity.set_color_map(val) + + +# Process args, removing them from the dict if they are present +def process_vector_args(structure, quantity, vector_args): + + val = check_and_pop_arg(vector_args, 'length') + if val is not None: + quantity.set_length(val, True) + + val = check_and_pop_arg(vector_args, 'radius') + if val is not None: + quantity.set_radius(val, True) + + val = check_and_pop_arg(vector_args, 'color') + if val is not None: + quantity.set_color(glm3(val)) + + +# Process args, removing them from the dict if they are present +def process_parameterization_args(structure, quantity, parameterization_args): + + val = check_and_pop_arg(parameterization_args, 'viz_style') + if val is not None: + viz_style_enum = str_to_param_viz_style(val) + quantity.set_style(viz_style_enum) + + val = check_and_pop_arg(parameterization_args, 'grid_colors') + if val is not None: + quantity.set_grid_colors((glm3(val[0]), glm3(val[1]))) + + val = check_and_pop_arg(parameterization_args, 'checker_colors') + if val is not None: + quantity.set_checker_colors((glm3(val[0]), glm3(val[1]))) + + val = check_and_pop_arg(parameterization_args, 'checker_size') + if val is not None: + quantity.set_checker_size(val) + + val = check_and_pop_arg(parameterization_args, 'cmap') + if val is not None: + quantity.set_color_map(val) + + val = check_and_pop_arg(parameterization_args, 'island_labels') + if val is not None: + if len(val.shape) != 1: raise ValueError("'island_labels' should be an (N_faces,) numpy array") + quantity.set_island_labels(val) + + val = check_and_pop_arg(parameterization_args, 'create_curve_network_from_seams') + if val is not None: + quantity.create_curve_network_from_seams(val) + + +# Process args, removing them from the dict if they are present +def process_image_args(structure, quantity, image_args): + + val = check_and_pop_arg(image_args, 'show_fullscreen') + if val is not None: + quantity.set_show_fullscreen(val) + + val = check_and_pop_arg(image_args, 'show_in_imgui_window') + if val is not None: + quantity.set_show_in_imgui_window(val) + + val = check_and_pop_arg(image_args, 'show_in_camera_billboard') + if val is not None: + quantity.set_show_in_camera_billboard(val) + + val = check_and_pop_arg(image_args, 'transparency') + if val is not None: + quantity.set_transparency(val) + +# Process args, removing them from the dict if they are present +def process_render_image_args(structure, quantity, image_args): + + val = check_and_pop_arg(image_args, 'transparency') + if val is not None: + quantity.set_transparency(val) + + val = check_and_pop_arg(image_args, 'material') + if val is not None: + quantity.set_material(val) + + val = check_and_pop_arg(image_args, 'allow_fullscreen_compositing') + if val is not None: + quantity.set_allow_fullscreen_compositing(val) + +# Process args, removing them from the dict if they are present +def process_implicit_render_args(opts, implicit_args): + + val = check_and_pop_arg(implicit_args, 'camera_parameters') + if val is not None: + opts.cameraParameters = val.instance + + val = check_and_pop_arg(implicit_args, 'dim') + if val is not None: + opts.dimX = int(val[0]) + opts.dimY = int(val[1]) + + val = check_and_pop_arg(implicit_args, 'subsample_factor') + if val is not None: + opts.subsampleFactor = int(val) + + val = check_and_pop_arg(implicit_args, 'miss_dist') + val_relative = check_and_pop_arg(implicit_args, 'miss_dist_relative') + if val is not None: + if val_relative is None: + val_relative = True + opts.set_missDist(float(val), val_relative) + + val = check_and_pop_arg(implicit_args, 'hit_dist') + val_relative = check_and_pop_arg(implicit_args, 'hit_dist_relative') + if val is not None: + if val_relative is None: + val_relative = True + opts.set_hitDist(float(val), val_relative) + + val = check_and_pop_arg(implicit_args, 'step_factor') + if val is not None: + opts.stepFactor = float(val) + + val = check_and_pop_arg(implicit_args, 'normal_sample_eps') + if val is not None: + opts.normalSampleEps = float(val) + + val = check_and_pop_arg(implicit_args, 'step_size') + val_relative = check_and_pop_arg(implicit_args, 'step_size_relative') + if val is not None: + if val_relative is None: + val_relative = True + opts.set_stepSize(float(val), val_relative) + + val = check_and_pop_arg(implicit_args, 'n_max_steps') + if val is not None: + opts.nMaxSteps= int(val) + + return opts + +def check_all_args_processed(structure, quantity, args): + for arg,val in args.items(): + raise ValueError(f"Polyscope: Unrecognized quantity keyword argument {arg}: {val}") + diff --git a/src/polyscope/core.py b/src/polyscope/core.py index 3200cc8..4411f9f 100644 --- a/src/polyscope/core.py +++ b/src/polyscope/core.py @@ -25,6 +25,26 @@ def show(forFrames=None): else: psb.show(forFrames) +def unshow(): + psb.unshow() + +def check_initialized(): + psb.check_initialized() + +def is_initialized(): + return psb.is_initialized() + +def frame_tick(): + psb.frame_tick() + +### Render engine + +def get_render_engine_backend_name(): + return psb.get_render_engine_backend_name() + +def get_key_code(c): + return psb.get_key_code(c) + ### Structure management def remove_all_structures(): @@ -61,9 +81,18 @@ def set_errors_throw_exceptions(val): def set_max_fps(f): psb.set_max_fps(f) +def set_enable_vsync(b): + psb.set_enable_vsync(b) + def set_use_prefs_file(v): psb.set_use_prefs_file(v) +def request_redraw(): + psb.request_redraw() + +def get_redraw_requested(): + return psb.get_redraw_requested() + def set_always_redraw(v): psb.set_always_redraw(v) @@ -79,6 +108,15 @@ def set_autoscale_structures(b): def set_build_gui(b): psb.set_build_gui(b) +def set_user_gui_is_on_right_side(b): + psb.set_user_gui_is_on_right_side(b) + +def set_build_default_gui_panels(b): + psb.set_build_default_gui_panels(b) + +def set_render_scene(b): + psb.set_render_scene(b) + def set_open_imgui_window_for_user_callback(b): psb.set_open_imgui_window_for_user_callback(b) @@ -88,11 +126,23 @@ def set_invoke_user_callback_for_nested_show(b): def set_give_focus_on_show(b): psb.set_give_focus_on_show(b) +def set_hide_window_after_show(b): + psb.set_hide_window_after_show(b) + def set_navigation_style(s): psb.set_navigation_style(str_to_navigate_style(s)) +def get_navigation_style(): + return navigate_style_to_str(psb.get_navigation_style()); def set_up_dir(d): psb.set_up_dir(str_to_updir(d)) +def get_up_dir(): + return updir_to_str(psb.get_up_dir()) + +def set_front_dir(d): + psb.set_front_dir(str_to_frontdir(d)) +def get_front_dir(): + return frontdir_to_str(psb.get_front_dir()) ### Scene extents @@ -126,6 +176,54 @@ def look_at_dir(camera_location, target, up_dir, fly_to=False): def set_view_projection_mode(s): psb.set_view_projection_mode(str_to_projection_mode(s)) +def set_window_size(width, height): + width = int(width) + height = int(height) + psb.set_window_size(width, height) + +def get_window_size(): + return psb.get_window_size() + +def get_buffer_size(): + return psb.get_buffer_size() + +def set_window_resizable(is_resizable): + psb.set_window_resizable(is_resizable) + +def get_window_resizable(): + return psb.get_window_resizable() + +def set_view_from_json(json_str, fly_to=False): + psb.set_view_from_json(json_str, fly_to) + +def get_view_as_json(): + return psb.get_view_as_json() + +def set_background_color(c): + if len(c) == 3: c = (c[0], c[1], c[2], 1.0) + psb.set_background_color(glm4(c)) + +def get_background_color(): + return psb.get_background_color() + +def get_view_camera_parameters(): + return CameraParameters(instance=psb.get_view_camera_parameters()) + +def set_view_camera_parameters(params): + if not isinstance(params, CameraParameters): raise ValueError("must pass CameraParameters") + psb.set_view_camera_parameters(params.instance) + +def get_view_buffer_resolution(): + return CameraParameters(instance=psb.get_view_camera_parameters()) + +def set_camera_view_matrix(mat): + mat = np.asarray(mat) + if mat.shape != (4,4): raise ValueError("mat should be a 4x4 numpy matrix") + psb.set_camera_view_matrix(mat) + +def get_camera_view_matrix(): + return psb.get_camera_view_matrix() + ### Messages def info(message): @@ -181,6 +279,68 @@ def set_transparency_render_passes(n): def set_SSAA_factor(n): psb.set_SSAA_factor(n) +## Groups + +class Group: + # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope + + # End users should not call this constrctor, use add_group() instead + def __init__(self, instance): + # Wrap an existing instance + self.bound_group = instance + + def get_name(self): + return self.bound_group.name + + def add_child_group(self, child_group): + if isinstance(child_group, str): + self.bound_group.add_child_group(get_group(child_group).bound_group) + else: + self.bound_group.add_child_group(child_group.bound_group) + + def add_child_structure(self, child_structure): + self.bound_group.add_child_structure(child_structure.bound_instance) + + def remove_child_group(self, child_group): + if isinstance(child_group, str): + self.bound_group.remove_child_group(get_group(child_group).bound_group) + else: + self.bound_group.remove_child_group(child_group.bound_group) + + def remove_child_structure(self, child_structure): + self.bound_group.remove_child_structure(child_structure.bound_instance) + + def set_enabled(self, new_val): + self.bound_group.set_enabled(new_val) + + def set_show_child_details(self, new_val): + self.bound_group.set_show_child_details(new_val) + + def set_hide_descendants_from_structure_lists(self, new_val): + self.bound_group.set_hide_descendants_from_structure_lists(new_val) + +def create_group(name): + return Group(psb.create_group(name)) + +def get_group(name): + return Group(psb.get_group(name)) + +def remove_group(group, error_if_absent=True): + # accept either a string or a group ref as input + if isinstance(group, str): + psb.remove_group(group, error_if_absent) + else: + psb.remove_group(group.get_name(), error_if_absent) + +def remove_all_groups(): + psb.remove_all_groups() + + +## Low-level internals access +# (warning, 'advanced' users only, may change) +def get_final_scene_color_texture_native_handle(): + return psb.get_final_scene_color_texture_native_handle() + ## Slice planes class SlicePlane: @@ -229,12 +389,90 @@ def add_scene_slice_plane(): def remove_last_scene_slice_plane(): psb.remove_last_scene_slice_plane() +### Camera Parameters + +class CameraIntrinsics: + + def __init__(self, fov_vertical_deg=None, fov_horizontal_deg=None, aspect=None, instance=None): + + if instance is not None: + self.instance = instance + elif fov_vertical_deg is not None and aspect is not None: + self.instance = psb.CameraIntrinsics.from_FoV_deg_vertical_and_aspect(float(fov_vertical_deg), float(aspect)) + elif fov_horizontal_deg is not None and aspect is not None: + self.instance = psb.CameraIntrinsics.from_FoV_deg_horizontal_and_aspect(float(fov_horizontal_deg), float(aspect)) + elif fov_vertical_deg is not None and fov_horizontal_deg is not None: + self.instance = psb.CameraIntrinsics.from_FoV_deg_horizontal_and_vertical(float(fov_horizontal_deg), float(fov_vertical_deg)) + else: + raise ValueError("bad arguments, at least two of (fov_vertical_deg,fov_horizontal_deg,aspect) must be given and non-None") + +class CameraExtrinsics: + + def __init__(self, root=None, look_dir=None, up_dir=None, mat=None, instance=None): + + if instance is not None: + self.instance = instance + + elif mat is not None: + mat = np.asarray(mat) + if mat.shape != (4,4): raise ValueError("mat should be a 4x4 numpy matrix") + self.instance = psb.CameraExtrinsics.from_matrix(mat) + + elif (root is not None) and (look_dir is not None) and (up_dir is not None): + + root = np.asarray(root) + look_dir = np.asarray(look_dir) + up_dir = np.asarray(up_dir) + self.instance = psb.CameraExtrinsics.from_vectors(root, look_dir, up_dir) + + else: + raise ValueError("bad arguments, must pass non-None (root,look_dir,up_dir) or non-None mat") + +class CameraParameters: + + def __init__(self, intrinsics=None, extrinsics=None, instance=None): + if instance is not None: + self.instance = instance + else: + self.instance = psb.CameraParameters(intrinsics.instance, extrinsics.instance) + + # getters + def get_intrinsics(self): return CameraIntrinsics(instance=self.instance.get_intrinsics()) + def get_extrinsics(self): return CameraExtrinsics(instance=self.instance.get_extrinsics()) + def get_T(self): return self.instance.get_T() + def get_R(self): return self.instance.get_R() + def get_view_mat(self): return self.instance.get_view_mat() # same as get_E() + def get_E(self): return self.instance.get_E() + def get_position(self): return self.instance.get_position() + def get_look_dir(self): return self.instance.get_look_dir() + def get_up_dir(self): return self.instance.get_up_dir() + def get_right_dir(self): return self.instance.get_right_dir() + def get_camera_frame(self): return self.instance.get_camera_frame() + def get_fov_vertical_deg(self): return self.instance.get_fov_vertical_deg() + def get_aspect(self): return self.instance.get_aspect() + + def generate_camera_rays(self, dims, image_origin='upper_left'): + out_rays = self.instance.generate_camera_rays( + int(dims[0]), int(dims[1]), + str_to_image_origin(image_origin) + ) + return out_rays.reshape(dims[0], dims[1], 3) + + def generate_camera_ray_corners(self): + return self.instance.generate_camera_ray_corners() + ## Small utilities +def glm3u(vals): + return psb.glm_uvec3(vals[0], vals[1], vals[2]) def glm3(vals): return psb.glm_vec3(vals[0], vals[1], vals[2]) def glm4(vals): return psb.glm_vec4(vals[0], vals[1], vals[2], vals[3]) +def degrees(val): + return 180.*val / np.PI +def radians(val): + return np.PI * val / 180. ### Materials @@ -265,18 +503,28 @@ def load_color_map(cmap_name, filename): ## String-to-enum translation +d_navigate = { + "turntable" : psb.NavigateStyle.turntable, + "free" : psb.NavigateStyle.free, + "planar" : psb.NavigateStyle.planar, + "none" : psb.NavigateStyle.none, + "first_person" : psb.NavigateStyle.first_person, +} def str_to_navigate_style(s): - d = { - "turntable" : psb.NavigateStyle.turntable, - "free" : psb.NavigateStyle.free, - "planar" : psb.NavigateStyle.planar, - } - if s not in d: + if s not in d_navigate: raise ValueError("Bad navigate style specifier '{}', should be one of [{}]".format(s, - ",".join(["'{}'".format(x) for x in d.keys()]))) + ",".join(["'{}'".format(x) for x in d_navigate.keys()]))) + + return d_navigate[s] +def navigate_style_to_str(val): + for k,v in d_navigate.items(): + if v == val: + return k + + raise ValueError("Bad navigate style specifier '{}', should be one of [{}]".format(val, + ",".join(["'{}'".format(x) for x in d_navigate.values()]))) - return d[s] def str_to_projection_mode(s): d = { @@ -290,21 +538,53 @@ def str_to_projection_mode(s): return d[s] -def str_to_updir(s): - d = { - "x_up" : psb.UpDir.x_up, - "neg_x_up" : psb.UpDir.neg_x_up, - "y_up" : psb.UpDir.y_up, - "neg_y_up" : psb.UpDir.neg_y_up, - "z_up" : psb.UpDir.z_up, - "neg_z_up" : psb.UpDir.neg_z_up, - } - if s not in d: +d_updir = { + "x_up" : psb.UpDir.x_up, + "neg_x_up" : psb.UpDir.neg_x_up, + "y_up" : psb.UpDir.y_up, + "neg_y_up" : psb.UpDir.neg_y_up, + "z_up" : psb.UpDir.z_up, + "neg_z_up" : psb.UpDir.neg_z_up, +} +def str_to_updir(s): + if s not in d_updir: raise ValueError("Bad up direction specifier '{}', should be one of [{}]".format(s, - ",".join(["'{}'".format(x) for x in d.keys()]))) + ",".join(["'{}'".format(x) for x in d_updir.keys()]))) - return d[s] + return d_updir[s] + +def updir_to_str(val): + for k,v in d_updir.items(): + if v == val: + return k + + raise ValueError("Bad up direction specifier '{}', should be one of [{}]".format(val, + ",".join(["'{}'".format(x) for x in d_updir.values()]))) + + +d_frontdir = { + "x_front" : psb.FrontDir.x_front, + "neg_x_front" : psb.FrontDir.neg_x_front, + "y_front" : psb.FrontDir.y_front, + "neg_y_front" : psb.FrontDir.neg_y_front, + "z_front" : psb.FrontDir.z_front, + "neg_z_front" : psb.FrontDir.neg_z_front, +} +def str_to_frontdir(s): + + if s not in d_frontdir: + raise ValueError("Bad front direction specifier '{}', should be one of [{}]".format(s, + ",".join(["'{}'".format(x) for x in d_frontdir.keys()]))) + + return d_frontdir[s] +def frontdir_to_str(val): + for k,v in d_frontdir.items(): + if v == val: + return k + + raise ValueError("Bad front direction specifier '{}', should be one of [{}]".format(val, + ",".join(["'{}'".format(x) for x in d_frontdir.values()]))) def str_to_datatype(s): d = { @@ -346,6 +626,7 @@ def str_to_param_coords_type(s): def str_to_param_viz_style(s): d = { "checker" : psb.ParamVizStyle.checker, + "checker_islands" : psb.ParamVizStyle.checker_islands, "grid" : psb.ParamVizStyle.grid, "local_check" : psb.ParamVizStyle.local_check, "local_rad" : psb.ParamVizStyle.local_rad, @@ -432,3 +713,73 @@ def point_render_mode_to_str(val): raise ValueError("Bad point render mode specifier '{}', should be one of [{}]".format(val, ",".join(["'{}'".format(x) for x in d_point_render_mode.values()]))) + +# Image origin to/from string +d_image_origin = { + "lower_left" : psb.ImageOrigin.lower_left, + "upper_left" : psb.ImageOrigin.upper_left, + } + +def str_to_image_origin(s): + + if s not in d_image_origin: + raise ValueError("Bad image origin specifier '{}', should be one of [{}]".format(s, + ",".join(["'{}'".format(x) for x in d_image_origin.keys()]))) + + return d_image_origin[s] + +def image_origin_to_str(val): + + for k,v in d_image_origin.items(): + if v == val: + return k + + raise ValueError("Bad image origin specifier '{}', should be one of [{}]".format(val, + ",".join(["'{}'".format(x) for x in d_image_origin.values()]))) + +# Shade style to/from string +d_mesh_shade_style = { + "smooth" : psb.MeshShadeStyle.smooth, + "flat" : psb.MeshShadeStyle.flat, + "tri_flat" : psb.MeshShadeStyle.tri_flat, + } + +def str_to_mesh_shade_style(s): + + if s not in d_mesh_shade_style: + raise ValueError("Bad specifier '{}', should be one of [{}]".format(s, + ",".join(["'{}'".format(x) for x in d_mesh_shade_style.keys()]))) + + return d_mesh_shade_style[s] + +def mesh_shade_style_to_str(val): + + for k,v in d_mesh_shade_style.items(): + if v == val: + return k + + raise ValueError("Bad specifier '{}', should be one of [{}]".format(val, + ",".join(["'{}'".format(x) for x in d_mesh_shade_style.values()]))) + +# Implicit render mode to/from string +d_implicit_render_mode = { + "fixed_step" : psb.ImplicitRenderMode.fixed_step, + "sphere_march" : psb.ImplicitRenderMode.sphere_march, + } + +def str_to_implicit_render_mode(s): + + if s not in d_implicit_render_mode: + raise ValueError("Bad specifier '{}', should be one of [{}]".format(s, + ",".join(["'{}'".format(x) for x in d_implicit_render_mode.keys()]))) + + return d_implicit_render_mode[s] + +def implicit_render_mode_to_str(val): + + for k,v in d_implicit_render_mode.items(): + if v == val: + return k + + raise ValueError("Bad specifier '{}', should be one of [{}]".format(val, + ",".join(["'{}'".format(x) for x in d_implicit_render_mode.values()]))) diff --git a/src/polyscope/curve_network.py b/src/polyscope/curve_network.py index ff422f4..3be899b 100644 --- a/src/polyscope/curve_network.py +++ b/src/polyscope/curve_network.py @@ -1,17 +1,21 @@ import polyscope_bindings as psb from polyscope.core import str_to_datatype, str_to_vectortype, glm3 +from polyscope.structure import Structure +from polyscope.common import process_quantity_args, process_scalar_args, process_color_args, process_vector_args, check_all_args_processed -class CurveNetwork: +class CurveNetwork(Structure): # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope # End users should not call this constrctor, use register_curve_network instead def __init__(self, name=None, nodes=None, edges=None, instance=None): + + super().__init__() if instance is not None: # Wrap an existing instance - self.bound_network = instance + self.bound_instance = instance else: # Create a new instance @@ -24,14 +28,14 @@ def __init__(self, name=None, nodes=None, edges=None, instance=None): if nodes.shape[1] == 3: if edges == 'line': - self.bound_network = psb.register_curve_network_line(name, nodes) + self.bound_instance = psb.register_curve_network_line(name, nodes) elif edges == 'loop': - self.bound_network = psb.register_curve_network_loop(name, nodes) + self.bound_instance = psb.register_curve_network_loop(name, nodes) elif nodes.shape[1] == 2: if edges == 'line': - self.bound_network = psb.register_curve_network_line2D(name, nodes) + self.bound_instance = psb.register_curve_network_line2D(name, nodes) elif edges == 'loop': - self.bound_network = psb.register_curve_network_loop2D(name, nodes) + self.bound_instance = psb.register_curve_network_loop2D(name, nodes) else: # Common case: process edges as numpy array @@ -39,9 +43,9 @@ def __init__(self, name=None, nodes=None, edges=None, instance=None): raise ValueError("curve network edges should have shape (N_edge,2); shape is " + str(edges.shape)) if nodes.shape[1] == 3: - self.bound_network = psb.register_curve_network(name, nodes, edges) + self.bound_instance = psb.register_curve_network(name, nodes, edges) elif nodes.shape[1] == 2: - self.bound_network = psb.register_curve_network2D(name, nodes, edges) + self.bound_instance = psb.register_curve_network2D(name, nodes, edges) def check_shape(self, points): # Helper to validate arrays @@ -49,79 +53,19 @@ def check_shape(self, points): raise ValueError("curve network node positions should have shape (N,3); shape is " + str(points.shape)) def n_nodes(self): - return self.bound_network.n_nodes() + return self.bound_instance.n_nodes() def n_edges(self): - return self.bound_network.n_edges() - + return self.bound_instance.n_edges() - ## Structure management - - def remove(self): - '''Remove the structure itself''' - self.bound_network.remove() - def remove_all_quantities(self): - '''Remove all quantities on the structure''' - self.bound_network.remove_all_quantities() - def remove_quantity(self, name): - '''Remove a single quantity on the structure''' - self.bound_network.remove_quantity(name) - - # Enable/disable - def set_enabled(self, val=True): - self.bound_network.set_enabled(val) - def is_enabled(self): - return self.bound_network.is_enabled() - - # Transparency - def set_transparency(self, val): - self.bound_network.set_transparency(val) - def get_transparency(self): - return self.bound_network.get_transparency() - - # Transformation things - def center_bounding_box(self): - self.bound_network.center_bounding_box() - def rescale_to_unit(self): - self.bound_network.rescale_to_unit() - def reset_transform(self): - self.bound_network.reset_transform() - def set_transform(self, new_mat4x4): - self.bound_network.set_transform(new_mat4x4) - def set_position(self, new_vec3): - self.bound_network.set_position(new_vec3) - def translate(self, trans_vec3): - self.bound_network.translate(trans_vec3) - def get_transform(self): - return self.bound_network.get_transform() - def get_position(self): - return self.bound_network.get_position() - - # Slice planes - def set_cull_whole_elements(self, val): - self.bound_network.set_cull_whole_elements(val) - def get_cull_whole_elements(self): - return self.bound_network.get_cull_whole_elements() - def set_ignore_slice_plane(self, plane, val): - # take either a string or a slice plane object as input - if isinstance(plane, str): - self.bound_network.set_ignore_slice_plane(plane, val) - else: - self.bound_network.set_ignore_slice_plane(plane.get_name(), val) - def get_ignore_slice_plane(self, plane): - # take either a string or a slice plane object as input - if isinstance(plane, str): - return self.bound_network.get_ignore_slice_plane(plane) - else: - return self.bound_network.get_ignore_slice_plane(plane.get_name()) # Update def update_node_positions(self, nodes): self.check_shape(nodes) if nodes.shape[1] == 3: - self.bound_network.update_node_positions(nodes) + self.bound_instance.update_node_positions(nodes) elif nodes.shape[1] == 2: - self.bound_network.update_node_positions2D(nodes) + self.bound_instance.update_node_positions2D(nodes) else: raise ValueError("bad node shape") @@ -129,70 +73,71 @@ def update_node_positions(self, nodes): # Radius def set_radius(self, rad, relative=True): - self.bound_network.set_radius(rad, relative) + self.bound_instance.set_radius(rad, relative) def get_radius(self): - return self.bound_network.get_radius() + return self.bound_instance.get_radius() # Color def set_color(self, val): - self.bound_network.set_color(glm3(val)) + self.bound_instance.set_color(glm3(val)) def get_color(self): - return self.bound_network.get_color().as_tuple() + return self.bound_instance.get_color().as_tuple() # Material def set_material(self, mat): - self.bound_network.set_material(mat) + self.bound_instance.set_material(mat) def get_material(self): - return self.bound_network.get_material() + return self.bound_instance.get_material() ## Quantities # Scalar - def add_scalar_quantity(self, name, values, defined_on='nodes', enabled=None, datatype="standard", vminmax=None, cmap=None): + def add_scalar_quantity(self, name, values, defined_on='nodes', datatype="standard", **scalar_args): if len(values.shape) != 1: raise ValueError("'values' should be a length-N array") if defined_on == 'nodes': if values.shape[0] != self.n_nodes(): raise ValueError("'values' should be a length n_nodes array") - q = self.bound_network.add_node_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_node_scalar_quantity(name, values, str_to_datatype(datatype)) elif defined_on == 'edges': if values.shape[0] != self.n_edges(): raise ValueError("'values' should be a length n_edges array") - q = self.bound_network.add_edge_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_edge_scalar_quantity(name, values, str_to_datatype(datatype)) else: raise ValueError("bad `defined_on` value {}, should be one of ['nodes', 'edges']".format(defined_on)) - + - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if vminmax is not None: - q.set_map_range(vminmax) - if cmap is not None: - q.set_color_map(cmap) + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, scalar_args) + process_scalar_args(self, q, scalar_args) + check_all_args_processed(self, q, scalar_args) # Color - def add_color_quantity(self, name, values, defined_on='nodes', enabled=None): + def add_color_quantity(self, name, values, defined_on='nodes', **color_args): if len(values.shape) != 2 or values.shape[1] != 3: raise ValueError("'values' should be an Nx3 array") if defined_on == 'nodes': if values.shape[0] != self.n_nodes(): raise ValueError("'values' should be a length n_nodes array") - q = self.bound_network.add_node_color_quantity(name, values) + q = self.bound_instance.add_node_color_quantity(name, values) elif defined_on == 'edges': if values.shape[0] != self.n_edges(): raise ValueError("'values' should be a length n_edges array") - q = self.bound_network.add_edge_color_quantity(name, values) + q = self.bound_instance.add_edge_color_quantity(name, values) else: raise ValueError("bad `defined_on` value {}, should be one of ['nodes', 'edges']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, color_args) + process_color_args(self, q, color_args) + check_all_args_processed(self, q, color_args) # Vector - def add_vector_quantity(self, name, values, defined_on='nodes', enabled=None, vectortype="standard", length=None, radius=None, color=None): + def add_vector_quantity(self, name, values, defined_on='nodes', vectortype="standard", **vector_args): if len(values.shape) != 2 or values.shape[1] not in [2,3]: raise ValueError("'values' should be an Nx3 array (or Nx2 for 2D)") @@ -200,35 +145,32 @@ def add_vector_quantity(self, name, values, defined_on='nodes', enabled=None, ve if values.shape[0] != self.n_nodes(): raise ValueError("'values' should be a length n_nodes array") if values.shape[1] == 2: - q = self.bound_network.add_node_vector_quantity2D(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_node_vector_quantity2D(name, values, str_to_vectortype(vectortype)) elif values.shape[1] == 3: - q = self.bound_network.add_node_vector_quantity(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_node_vector_quantity(name, values, str_to_vectortype(vectortype)) elif defined_on == 'edges': if values.shape[0] != self.n_edges(): raise ValueError("'values' should be a length n_edges array") if values.shape[1] == 2: - q = self.bound_network.add_edge_vector_quantity2D(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_edge_vector_quantity2D(name, values, str_to_vectortype(vectortype)) elif values.shape[1] == 3: - q = self.bound_network.add_edge_vector_quantity(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_edge_vector_quantity(name, values, str_to_vectortype(vectortype)) else: raise ValueError("bad `defined_on` value {}, should be one of ['nodes', 'edges']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if length is not None: - q.set_length(length, True) - if radius is not None: - q.set_radius(radius, True) - if color is not None: - q.set_color(glm3(color)) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, vector_args) + process_vector_args(self, q, vector_args) + check_all_args_processed(self, q, vector_args) def register_curve_network(name, nodes, edges, enabled=None, radius=None, color=None, material=None, transparency=None): """Register a new curve network""" - if not psb.isInitialized(): + if not psb.is_initialized(): raise RuntimeError("Polyscope has not been initialized") p = CurveNetwork(name, nodes, edges) diff --git a/src/polyscope/device_interop.py b/src/polyscope/device_interop.py new file mode 100644 index 0000000..ae93622 --- /dev/null +++ b/src/polyscope/device_interop.py @@ -0,0 +1,431 @@ +import polyscope_bindings as psb + +import os, sys +from functools import partial + +import numpy as np + +from polyscope.core import get_render_engine_backend_name + +device_interop_funcs = None + +############################################################################# +### Default CUDA implementation of interop functions +############################################################################# + +# Device interoperability requires directly copying data from user's arrays +# on the GPU into Polyscope's openGL buffers. This cannot be done directly +# through openGL, it requires a memcopy from device compute library (generally CUDA). +# +# Unfortunately, CUDA is is a nontrivial dependency, and many things could go wrong +# with installation and mismatched versions. To give more flexibility, all of the +# function calls related to the CUDA memcpy are abstracted as a dictionary of callbacks; +# as long as this dictionary is populated, Polyscope can call the functions to access +# user arrays and do the copies. +# +# There are two options for users to get the needed functions +# +# - [Default Case] The python packages `cuda` and `cupy` can be installed as additional +# optional dependencies. They offer the necessary CUDA functions. This is the default +# path if device interop functions are used. The result is that any array type which +# implements __cuda_array_interface__ or __dlpack__ (aka almost all libraries) +# can be automatically read from. +# +# - [Custom Case] In some cases, the `cuda` and `cupy` packages may not install correctly, +# or the user's own codebase may already have its own preferred bindings to cuda functions. +# in this case the user can call set_device_interop_funcs() once, and pass a dictionary +# with a handful of callbacks to do the necessary mapping and copying. See below for +# the meaning of these callbacks. + +def ensure_device_interop_funcs_resolve(): + check_device_module_availibility() + if device_interop_funcs is not None: + return + resolve_default_device_interop_funcs() + +def set_device_interop_funcs(func_dict): + global device_interop_funcs + device_interop_funcs = func_dict + +def resolve_default_device_interop_funcs(): + + # Try both of these imports, but fail silently if they don't work (we will try + # again and print an informative error below only if a relevant function is called) + try: + import cuda + from cuda import cudart + import cupy + except ImportError: + raise ImportError('This Polyscope functionality requires cuda bindings to be installed. Please install the packages `cuda` and `cupy`. Try `python -m pip install cuda-python cupy`. See https://nvidia.github.io/cuda-python/ & https://cupy.dev/.') + + + # TODO is it possible to implement this without relying on exceptions? + ''' + def is_dlpack(obj): + return hasattr(obj, '__dlpack__') and hasattr(obj, '__dlpack_device__') + ''' + + def is_cuda_array_interface(obj): + return hasattr(obj, '__cuda_array_interface__') + + + def format_cudart_err(err): + return ( + f"{cudart.cudaGetErrorName(err)[1].decode('utf-8')}({int(err)}): " + f"{cudart.cudaGetErrorString(err)[1].decode('utf-8')}" + ) + + + # helper function: check errors + def check_cudart_err(args): + if isinstance(args, tuple): + assert len(args) >= 1 + err = args[0] + if len(args) == 1: + ret = None + elif len(args) == 2: + ret = args[1] + else: + ret = args[1:] + else: + err = args + ret = None + + assert isinstance(err, cudart.cudaError_t), type(err) + if err != cudart.cudaError_t.cudaSuccess: + raise RuntimeError(format_cudart_err(err)) + + return ret + + # helper function: dispatch to one of the kinds of objects that we can read from + def get_array_from_unknown_data(arr): + + # __cuda_array_interface__ + if is_cuda_array_interface(arr): + cupy_arr = cupy.ascontiguousarray(cupy.asarray(arr)) + + else: + # __dlpack__ + # (I can't figure out any way to check this except try-catch) + try: + cupy_arr = cupy.ascontiguousarray(cupy.from_dlpack(arr)) + except ValueError: + pass + + raise ValueError("Cannot read from device data object. Must be a _dlpack_ array or implement the __cuda_array_interface__.") + + shape = cupy_arr.shape + dtype = cupy_arr.dtype + n_bytes = cupy_arr.nbytes + + return cupy_arr.data.ptr, shape, dtype, n_bytes + + def map_resource_and_get_array(handle): + check_cudart_err(cudart.cudaGraphicsMapResources(1, handle, None)), + return check_cudart_err(cudart.cudaGraphicsSubResourceGetMappedArray(handle, 0, 0)) + + def map_resource_and_get_pointer(handle): + check_cudart_err(cudart.cudaGraphicsMapResources(1, handle, None)), + raw_ptr, size = check_cudart_err(cudart.cudaGraphicsResourceGetMappedPointer(handle)) + return raw_ptr, size + + func_dict = { + + # returns tuple `(desc, extent, flags)`, as in cudaArrayGetInfo() + # this function is optional, and only used for sanity checks it can be left undefined + 'get_array_info' : lambda array: + check_cudart_err( + cudart.cudaArrayGetInfo(array) + ), + + # as cudaGraphicsUnmapResources(1, handle, None) + 'unmap_resource' : lambda handle : + check_cudart_err(cudart.cudaGraphicsUnmapResources(1, handle, None)), + + + # returns a registered handle + # as cudaGraphicsGLRegisterBuffer() + 'register_gl_buffer' : lambda native_id : + check_cudart_err(cudart.cudaGraphicsGLRegisterBuffer( + native_id, + cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard + )), + + + # returns a registered handle + # as in cudaGraphicsGLRegisterImage () + 'register_gl_image_2d' : lambda native_id : + check_cudart_err(cudart.cudaGraphicsGLRegisterImage( + native_id, + _CONSTANT_GL_TEXTURE_2D, + cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard + )), + + + # as in cudaGraphicsUnregisterResource() + 'unregister_resource' : lambda handle : + check_cudart_err(cudart.cudaGraphicsUnregisterResource(handle)), + + + # returns array + # as in cudaGraphicsMapResources() + cudaGraphicsSubResourceGetMappedArray() + 'map_resource_and_get_array' : map_resource_and_get_array, + + # returns ptr + # as in cudaGraphicsMapResources() + cudaGraphicsResourceGetMappedPointer() + 'map_resource_and_get_pointer' : map_resource_and_get_pointer, + + + # returns a tuple (arr_ptr, shape, dtype, nbytes) + # The last three entries are all optional and can be None. If given they will be used for additional sanity checks. + 'get_array_ptr' : lambda input_array: + get_array_from_unknown_data(input_array), + + # as in cudaMemcpy + 'memcpy' : lambda dst_ptr, src_ptr, size : + check_cudart_err(cuda.cudart.cudaMemcpy( + dst_ptr, src_ptr, size, cudart.cudaMemcpyKind.cudaMemcpyDeviceToDevice + )), + + + # as in cudaMemcpy2DToArray + 'memcpy_2d' : lambda dst_ptr, src_ptr, width, height : + check_cudart_err(cuda.cudart.cudaMemcpy2DToArray( + dst_ptr, 0, 0, src_ptr, width, width, height, cudart.cudaMemcpyKind.cudaMemcpyDeviceToDevice + )), + + } + + set_device_interop_funcs(func_dict) + + +# This seems absurd! These constants are needed for cuda.cudart calls, but there is apparently no canonical way to get them without introducing an additional dependency. +# See discussion https://discourse.panda3d.org/t/allowing-torch-tensorflow-to-directly-access-rendered-image-in-gpu/29119/3 +# As a workaround, we hardcord their constant values, fetched from https://cvs.khronos.org/svn/repos/ogl/trunk/doc/registry/public/api/GL/glcorearb.h +_CONSTANT_GL_TEXTURE_2D = int('0DE1',16) +_CONSTANT_GL_TEXTURE_3D = int('806F',16) + + + +############################################################################# +### Mapped buffer interop interface +############################################################################# + +def check_device_module_availibility(): + + supported_backends = ["openGL3_glfw"] + if get_render_engine_backend_name() not in supported_backends: + raise ValueError(f"This Polyscope functionality is not supported by the current rendering backend ({get_render_engine_backend_name()}. Supported backends: {','.join(supported_backends)}.") + + +class CUDAOpenGLMappedAttributeBuffer: + + # Roughly based on this, see for more goodies: https://gist.github.com/keckj/e37d312128eac8c5fca790ce1e7fc437 + + def __init__(self, gl_attribute_native_id, buffer_type): + + self.gl_attribute_native_id = gl_attribute_native_id + self.buffer_type = buffer_type + self.resource_handle = None + self.cuda_buffer_ptr = None + self.cuda_buffer_size = -1 + self.finished_init = False + + ensure_device_interop_funcs_resolve() + + # Sanity checks + if self.buffer_type != psb.DeviceBufferType.attribute: + raise ValueError("device buffer type should be attribute") + + + # Register the buffer + self.resource_handle = device_interop_funcs['register_gl_buffer'](self.gl_attribute_native_id) + + self.finished_init = True + + def __del__(self): + if self.finished_init: + self.unregister() + + def unregister(self): + self.unmap() + device_interop_funcs['unregister_resource'](self.resource_handle) + + def map(self): + """ + Returns a cupy memory pointer to the buffer + """ + + if self.cuda_buffer_ptr is not None: + return + + self.cuda_buffer_ptr, self.cuda_buffer_size = device_interop_funcs['map_resource_and_get_pointer'](self.resource_handle) + + def unmap(self): + if not hasattr(self, 'cuda_buffer_ptr') or self.cuda_buffer_ptr is None: + return + + device_interop_funcs['unmap_resource'](self.resource_handle) + + self.cuda_buffer_ptr = None + self.cuda_buffer_size = -1 + + def set_data_from_array(self, arr, buffer_size_in_bytes=None, expected_shape=None, expected_dtype=None): + + self.map() + + # cupy_arr = self.get_array_from_unknown_data(arr) + + # access the input array + arr_ptr, arr_shape, arr_dtype, arr_nbytes = device_interop_funcs['get_array_ptr'](arr) + + if arr_nbytes is not None and arr_nbytes != self.cuda_buffer_size: + # if cupy_arr has the right size/dtype, it should have exactly the same + # number of bytes as the destination. This is just lazily saving us + # from repeating the math, and also directly validates the copy we + # are about to do below. + raise ValueError(f"Mapped buffer write has wrong size, expected {arr_nbytes} bytes but got {self.cuda_buffer_size} bytes. Could it be the wrong size/shape or wrong dtype?") + + + # perform the actual copy + device_interop_funcs['memcpy'](self.cuda_buffer_ptr, arr_ptr, self.cuda_buffer_size) + + self.unmap() + + +class CUDAOpenGLMappedTextureBuffer: + + + def __init__(self, gl_attribute_native_id, buffer_type): + + self.gl_attribute_native_id = gl_attribute_native_id + self.buffer_type = buffer_type + self.resource_handle = None + self.cuda_buffer_array = None # NOTE: 'array' has a special cuda meaning here relating to texture memory + self.finished_init = False + + ensure_device_interop_funcs_resolve() + + # Register the buffer + + if self.buffer_type == psb.DeviceBufferType.attribute: + raise ValueError("type should be texture*") + + elif self.buffer_type == psb.DeviceBufferType.texture1d: + raise ValueError("1d texture writes are not supported") + # apparently not supported (?!) + # see cudaGraphicsGLRegisterImage in these docs https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__OPENGL.html + + elif self.buffer_type == psb.DeviceBufferType.texture2d: + self.resource_handle = device_interop_funcs['register_gl_image_2d'](self.gl_attribute_native_id) + + elif self.buffer_type == psb.DeviceBufferType.texture3d: + # TODO + raise ValueError("3d texture writes are not implemented") + + + self.finished_init = True + + def __del__(self): + if self.finished_init: + self.unregister() + + def unregister(self): + self.unmap() + device_interop_funcs['unregister_resource'](self.resource_handle) + + def map(self): + if self.cuda_buffer_array is not None: + return + + self.cuda_buffer_array = device_interop_funcs['map_resource_and_get_array'](self.resource_handle) + + + def unmap(self): + if not hasattr(self, 'cuda_buffer_array') or self.cuda_buffer_array is None: + return + + device_interop_funcs['unmap_resource'](self.resource_handle) + + self.cuda_buffer_array = None + + def set_data_from_array(self, arr, texture_dims, entry_size_in_bytes): + + self.map() + + # get some info about the buffer we just mapped + # NOTE: this used to be necessary to get the size of the copy, but now it is optional and just use it for sanity checking + + extent_tup = None + if 'get_array_info' in device_interop_funcs: + extent_tup = device_interop_funcs['get_array_info'](self.cuda_buffer_array) + + # access the input array + arr_ptr, arr_shape, arr_dtype, arr_nbytes = device_interop_funcs['get_array_ptr'](arr) + + + if self.buffer_type == psb.DeviceBufferType.texture2d: + + # if we got extent info from the destination array, use it to do additional sanity checks + if extent_tup is not None: + + desc, extent, flags = extent_tup + dst_buff_width = extent.width + dst_buff_height = extent.height + dst_buff_width_pad = 0 + + dst_buff_bytes_per = (desc.x + desc.y + desc.z + desc.w) // 8 + dst_buff_width_in_bytes = dst_buff_width * dst_buff_bytes_per + + dst_n_cmp = (1 if desc.x > 0 else 0) + \ + (1 if desc.y > 0 else 0) + \ + (1 if desc.z > 0 else 0) + \ + (1 if desc.w > 0 else 0) + + dst_n_elems = dst_buff_width*dst_buff_height*dst_n_cmp + + if extent.width != texture_dims[0]: raise ValueError(f"Mapped buffer width mismatch, {extent.width} vs. {texture_dims[0]}.") + if extent.height != texture_dims[1]: raise ValueError(f"Mapped buffer height mismatch, {extent.height} vs. {texture_dims[1]}.") + if extent.depth != texture_dims[2]: raise ValueError(f"Mapped buffer depth mismatch, {extent.depth} vs. {texture_dims[2]}.") + if dst_buff_bytes_per != entry_size_in_bytes : raise ValueError(f"Mapped buffer entry byte size mismatch, {dst_buff_bytes_per} vs. {entry_size_in_bytes}.") + + + # if we got shape info from the source array, use it to do sanity checks + if arr_shape is not None: + # TODO this test is broken as-written, doesn't account for presence/asbsence of channel count + pass + + # # size of the input array + # arr_size = 1 + # for s in arr_shape: arr_size *= s + # + # # size of the destination array + # dst_size = 1 + # for s in texture_dims: + # if s > 0: dst_size *= s + # + # if arr_size != dst_size: + # raise ValueError(f"Mapped buffer write has wrong size, destination buffer has {dst_size} elements, but source buffer has {arr_size}.") + + + # if we got bytesize info from the source AND destination array, use it to do more sanity checks + if arr_nbytes is not None and extent_tup is not None: + expected_bytes = dst_buff_width * dst_buff_height * dst_buff_bytes_per + if arr_nbytes != expected_bytes: + # if cupy_arr has the right size/dtype, it should have exactly the same + # number of bytes as the destination. This is just lazily saving us + # from repeating the math, and also directly validates the copy we + # are about to do below. + raise ValueError(f"Mapped buffer write has wrong size, expected {arr_nbytes} bytes but got {expected_bytes} bytes. Could it be the wrong size/shape or wrong dtype?") + + + device_interop_funcs['memcpy_2d']( + self.cuda_buffer_array, arr_ptr, texture_dims[0]*entry_size_in_bytes, texture_dims[1] + ) + + else: + raise ValueError("not implemented") + + self.unmap() + + diff --git a/src/polyscope/floating_quantities.py b/src/polyscope/floating_quantities.py new file mode 100644 index 0000000..ba0a3d5 --- /dev/null +++ b/src/polyscope/floating_quantities.py @@ -0,0 +1,226 @@ +import polyscope_bindings as psb + +from polyscope.common import check_is_scalar_image, check_is_image3, check_is_image4, check_image_dims_compatible, process_scalar_args, process_color_args, process_image_args, process_render_image_args, process_quantity_args, check_all_args_processed +from polyscope.core import str_to_image_origin, str_to_datatype, glm3 + +import numpy as np + +def _resolve_floating_struct_instance(struct_ref): + if struct_ref is None: + return psb.get_global_floating_quantity_structure() + else: + return struct_ref.bound_instance + + +def get_quantity_buffer(quantity_name, buffer_name): + from polyscope.global_floating_quantity_structure import FloatingQuantityStructure + + floating_struct = FloatingQuantityStructure() + return floating_struct.get_quantity_buffer(quantity_name, buffer_name) + + +def add_scalar_image_quantity(name, values, image_origin="upper_left", datatype="standard", struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_scalar_image(values) + dimY = values.shape[0] + dimX = values.shape[1] + + values_flat = values.flatten() + + q = struct_instance_ref.add_scalar_image_quantity(name, dimX, dimY, values_flat, + str_to_image_origin(image_origin), str_to_datatype(datatype)) + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_image_args(struct_ref, q, option_args) + process_scalar_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + +def add_color_image_quantity(name, values, image_origin="upper_left", struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_image3(values) + dimY = values.shape[0] + dimX = values.shape[1] + + values_flat = values.reshape(-1,3) + + q = struct_instance_ref.add_color_image_quantity(name, dimX, dimY, values_flat, + str_to_image_origin(image_origin)) + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_image_args(struct_ref, q, option_args) + process_color_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + + +def add_color_alpha_image_quantity(name, values, image_origin="upper_left", struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_image4(values) + dimY = values.shape[0] + dimX = values.shape[1] + + values_flat = values.reshape(-1,4) + + q = struct_instance_ref.add_color_alpha_image_quantity(name, dimX, dimY, values_flat, + str_to_image_origin(image_origin)) + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_image_args(struct_ref, q, option_args) + process_color_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + + +def add_depth_render_image_quantity(name, depth_values, normal_values, image_origin="upper_left", color=None, struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_scalar_image(depth_values) + if normal_values is None: + normal_values = np.zeros((0,0,3)) + else: + check_is_image3(normal_values) + check_image_dims_compatible([depth_values, normal_values]) + dimY = depth_values.shape[0] + dimX = depth_values.shape[1] + + depth_values_flat = depth_values.flatten() + normal_values_flat = normal_values.reshape(-1,3) + + q = struct_instance_ref.add_depth_render_image_quantity(name, dimX, dimY, + depth_values_flat, normal_values_flat, + str_to_image_origin(image_origin)) + + if color is not None: + q.set_color(glm3(color)) + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + + +def add_color_render_image_quantity(name, depth_values, normal_values, color_values, image_origin="upper_left", struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_scalar_image(depth_values) + check_is_image3(color_values) + if normal_values is None: + normal_values = np.zeros((0,0,3)) + check_image_dims_compatible([depth_values, color_values]) + else: + check_is_image3(normal_values) + check_image_dims_compatible([depth_values, normal_values, color_values]) + dimY = depth_values.shape[0] + dimX = depth_values.shape[1] + + depth_values_flat = depth_values.flatten() + normal_values_flat = normal_values.reshape(-1,3) + color_values_flat = color_values.reshape(-1,3) + + q = struct_instance_ref.add_color_render_image_quantity(name, dimX, dimY, + depth_values_flat, normal_values_flat, color_values_flat, + str_to_image_origin(image_origin)) + + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_color_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + + +def add_scalar_render_image_quantity(name, depth_values, normal_values, scalar_values, image_origin="upper_left", struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_scalar_image(depth_values) + check_is_scalar_image(scalar_values) + if normal_values is None: + normal_values = np.zeros((0,0,3)) + check_image_dims_compatible([depth_values, scalar_values]) + else: + check_is_image3(normal_values) + check_image_dims_compatible([depth_values, normal_values, scalar_values]) + dimY = depth_values.shape[0] + dimX = depth_values.shape[1] + + depth_values_flat = depth_values.flatten() + normal_values_flat = normal_values.reshape(-1,3) + scalar_values_flat = scalar_values.flatten() + + q = struct_instance_ref.add_scalar_render_image_quantity(name, dimX, dimY, + depth_values_flat, normal_values_flat, scalar_values_flat, + str_to_image_origin(image_origin)) + + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_scalar_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + + +def add_raw_color_render_image_quantity(name, depth_values, color_values, image_origin="upper_left", struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_scalar_image(depth_values) + check_is_image3(color_values) + check_image_dims_compatible([depth_values, color_values]) + dimY = depth_values.shape[0] + dimX = depth_values.shape[1] + + depth_values_flat = depth_values.flatten() + color_values_flat = color_values.reshape(-1,3) + + q = struct_instance_ref.add_raw_color_render_image_quantity(name, dimX, dimY, + depth_values_flat, color_values_flat, + str_to_image_origin(image_origin)) + + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_color_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + +def add_raw_color_alpha_render_image_quantity(name, depth_values, color_values, image_origin="upper_left", struct_ref=None, **option_args): + + struct_instance_ref = _resolve_floating_struct_instance(struct_ref) + + check_is_scalar_image(depth_values) + check_is_image4(color_values) + check_image_dims_compatible([depth_values, color_values]) + dimY = depth_values.shape[0] + dimX = depth_values.shape[1] + + depth_values_flat = depth_values.flatten() + color_values_flat = color_values.reshape(-1,4) + + q = struct_instance_ref.add_raw_color_alpha_render_image_quantity(name, dimX, dimY, + depth_values_flat, color_values_flat, + str_to_image_origin(image_origin)) + + + # process and act on additional arguments + # note: each step modifies the option_args dict and removes processed args + process_quantity_args(struct_ref, q, option_args) + process_color_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) diff --git a/src/polyscope/global_floating_quantity_structure.py b/src/polyscope/global_floating_quantity_structure.py new file mode 100644 index 0000000..ef87759 --- /dev/null +++ b/src/polyscope/global_floating_quantity_structure.py @@ -0,0 +1,13 @@ +import polyscope_bindings as psb + +from polyscope.core import str_to_datatype, str_to_vectortype, glm3, str_to_point_render_mode, point_render_mode_to_str +from polyscope.structure import Structure + +class FloatingQuantityStructure(Structure): + + # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope + + def __init__(self): + super().__init__() + self.bound_instance = psb.get_global_floating_quantity_structure() + diff --git a/src/polyscope/implicit_helpers.py b/src/polyscope/implicit_helpers.py new file mode 100644 index 0000000..b9d8af2 --- /dev/null +++ b/src/polyscope/implicit_helpers.py @@ -0,0 +1,94 @@ +import polyscope_bindings as psb + +from polyscope.common import check_is_scalar_image, check_is_image3, check_is_image4, check_image_dims_compatible, process_scalar_args, process_color_args, process_image_args, process_render_image_args, process_quantity_args, process_implicit_render_args, check_all_args_processed + +from polyscope.core import str_to_image_origin, str_to_datatype, str_to_implicit_render_mode, glm3 + + + +def render_implicit_surface(name, func, mode, camera_view=None, color=None, **option_args): + + # prep args + mode_str = str_to_implicit_render_mode(mode) + opts = psb.ImplicitRenderOpts() + opts = process_implicit_render_args(opts, option_args) + + if camera_view is None: + struct_ref = psb.get_global_floating_quantity_structure() + cam = None + else: + struct_ref = camera_view + cam = camera_view.bound_instance + + q = psb.render_implicit_surface_batch(name, func, mode_str, opts, cam) + + if color is not None: + q.set_color(glm3(color)) + + process_quantity_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + +def render_implicit_surface_color(name, func, func_color, mode, camera_view=None, **option_args): + + # prep args + mode_str = str_to_implicit_render_mode(mode) + opts = psb.ImplicitRenderOpts() + opts = process_implicit_render_args(opts, option_args) + + if camera_view is None: + struct_ref = psb.get_global_floating_quantity_structure() + cam = None + else: + struct_ref = camera_view + cam = camera_view.bound_instance + + q = psb.render_implicit_surface_color_batch(name, func, func_color, mode_str, opts, cam) + + process_quantity_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + process_color_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + +def render_implicit_surface_scalar(name, func, func_scalar, mode, camera_view=None, **option_args): + + # prep args + mode_str = str_to_implicit_render_mode(mode) + opts = psb.ImplicitRenderOpts() + opts = process_implicit_render_args(opts, option_args) + + if camera_view is None: + struct_ref = psb.get_global_floating_quantity_structure() + cam = None + else: + struct_ref = camera_view + cam = camera_view.bound_instance + + q = psb.render_implicit_surface_scalar_batch(name, func, func_scalar, mode_str, opts, cam) + + process_quantity_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + process_scalar_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + +def render_implicit_surface_raw_color(name, func, func_color, mode, camera_view=None, **option_args): + + # prep args + mode_str = str_to_implicit_render_mode(mode) + opts = psb.ImplicitRenderOpts() + opts = process_implicit_render_args(opts, option_args) + + if camera_view is None: + struct_ref = psb.get_global_floating_quantity_structure() + cam = None + else: + struct_ref = camera_view + cam = camera_view.bound_instance + + q = psb.render_implicit_surface_raw_color_batch(name, func, func_color, mode_str, opts, cam) + + process_quantity_args(struct_ref, q, option_args) + process_render_image_args(struct_ref, q, option_args) + process_color_args(struct_ref, q, option_args) + check_all_args_processed(struct_ref, q, option_args) + diff --git a/src/polyscope/managed_buffer.py b/src/polyscope/managed_buffer.py new file mode 100644 index 0000000..787844a --- /dev/null +++ b/src/polyscope/managed_buffer.py @@ -0,0 +1,144 @@ +import polyscope_bindings as psb + +import numpy as np + +from polyscope.device_interop import CUDAOpenGLMappedAttributeBuffer, CUDAOpenGLMappedTextureBuffer + +# A cache of mapped buffers +_mapped_buffer_cache_CUDAOpenGL = {} # maps uniqueID --> buffer object + +class ManagedBuffer: + + # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope + + # End users should not call this constructor, use structure.get_buffer("name") or another like it + def __init__(self, instance, buffer_type): + + self.uniqueID = None # gets overwritten below, setting early so it is defined if __del__ gets called + + self.bound_buffer = instance + self.buffer_type = buffer_type + self.device_buffer_type = self.bound_buffer.get_device_buffer_type() + + self.buffer_weak_ref = self.bound_buffer.get_generic_weak_handle() + self.uniqueID = self.buffer_weak_ref.get_unique_ID() + + def __del__(self): + self.release_mapped_buffer_CUDAOpenGL() + + def check_ref_still_valid(self): + if not self.buffer_weak_ref.is_valid(): + raise ValueError("[polyscope] lifetime of underlying buffer has expired") + + def size(self): + self.check_ref_still_valid() + + return self.bound_buffer.size() + + def get_texture_size(self): + self.check_ref_still_valid() + + return self.bound_buffer.get_texture_size() + + def has_data(self): + self.check_ref_still_valid() + + return self.bound_buffer.has_data() + + def summary_string(self): + self.check_ref_still_valid() + return self.bound_buffer.summary_string() + + def get_value(self, ind, indY=None, indZ=None): + self.check_ref_still_valid() + + # warning: expensive, don't call it in a loop + if indZ is not None: + return self.bound_buffer.get_value(ind, indY, indZ) + if indY is not None: + return self.bound_buffer.get_value(ind, indY) + return self.bound_buffer.get_value(ind) + + def update_data(self, new_vals): + self.check_ref_still_valid() + + new_vals = new_vals.reshape((self.size(),-1)) + + self.update_data_from_host(new_vals) + + def update_data_from_host(self, new_vals): + self.check_ref_still_valid() + + # TODO: this actually calls functions with different signatures based on + # what the underlying kind of buffer is. We should probably document it + # better or provide some error checking at the Python level. + # NOTE: this method calls mark_host_buffer_updated() internally, so there is no need to call it again + self.bound_buffer.update_data(new_vals) + + def update_data_from_device(self, new_vals_device): + self.check_ref_still_valid() + + mapped_buffer = self.get_mapped_buffer_CUDAOpenGL() + + if self.device_buffer_type == psb.DeviceBufferType.attribute: + + mapped_buffer.set_data_from_array(new_vals_device, self.bound_buffer.get_device_buffer_size_in_bytes()) + + self.bound_buffer.mark_render_attribute_buffer_updated() + + else: # texture + + mapped_buffer.set_data_from_array(new_vals_device, self.bound_buffer.get_texture_size(), + self.bound_buffer.get_device_buffer_element_size_in_bytes()) + + self.bound_buffer.mark_render_texture_buffer_updated() + + def get_mapped_buffer_CUDAOpenGL(self): + self.check_ref_still_valid() + + if self.uniqueID not in _mapped_buffer_cache_CUDAOpenGL: + # create a new one + if self.device_buffer_type == psb.DeviceBufferType.attribute: + nativeID = self.bound_buffer.get_native_render_attribute_buffer_ID() + + _mapped_buffer_cache_CUDAOpenGL[self.uniqueID] = \ + CUDAOpenGLMappedAttributeBuffer( + nativeID, + self.device_buffer_type + ) + + else: # texture + nativeID = self.bound_buffer.get_native_render_texture_buffer_ID() + + _mapped_buffer_cache_CUDAOpenGL[self.uniqueID] = \ + CUDAOpenGLMappedTextureBuffer( + nativeID, + self.device_buffer_type + ) + + + return _mapped_buffer_cache_CUDAOpenGL[self.uniqueID] + + def release_mapped_buffer_CUDAOpenGL(self): + if self.uniqueID in _mapped_buffer_cache_CUDAOpenGL: + del _mapped_buffer_cache_CUDAOpenGL[self.uniqueID] + + def get_texture_native_id(self): + if self.device_buffer_type == psb.DeviceBufferType.attribute: + raise ValueError("buffer is not a texture (perhaps try the 'attribute' variant?)") + + return self.bound_buffer.get_native_render_texture_buffer_ID() + + def get_attribute_native_id(self): + if self.device_buffer_type != psb.DeviceBufferType.attribute: + raise ValueError("buffer is not an attribute (perhaps try the 'texture' variant?)") + + return self.bound_buffer.get_native_render_attribute_buffer_ID() + + + def mark_device_buffer_updated(self): + + if self.device_buffer_type == psb.DeviceBufferType.attribute: + self.bound_buffer.mark_render_attribute_buffer_updated() + else: # texture + self.bound_buffer.mark_render_texture_buffer_updated() diff --git a/src/polyscope/point_cloud.py b/src/polyscope/point_cloud.py index 2cd2eb1..8098271 100644 --- a/src/polyscope/point_cloud.py +++ b/src/polyscope/point_cloud.py @@ -1,17 +1,21 @@ import polyscope_bindings as psb from polyscope.core import str_to_datatype, str_to_vectortype, glm3, str_to_point_render_mode, point_render_mode_to_str +from polyscope.structure import Structure +from polyscope.common import process_quantity_args, process_scalar_args, process_color_args, process_vector_args, process_parameterization_args, check_all_args_processed -class PointCloud: +class PointCloud(Structure): # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope # End users should not call this constrctor, use register_point_cloud instead def __init__(self, name=None, points=None, instance=None): + super().__init__() + if instance is not None: # Wrap an existing instance - self.bound_cloud = instance + self.bound_instance = instance else: # Create a new instance @@ -19,9 +23,9 @@ def __init__(self, name=None, points=None, instance=None): self.check_shape(points) if points.shape[1] == 3: - self.bound_cloud = psb.register_point_cloud(name, points) + self.bound_instance = psb.register_point_cloud(name, points) elif points.shape[1] == 2: - self.bound_cloud = psb.register_point_cloud2D(name, points) + self.bound_instance = psb.register_point_cloud2D(name, points) else: raise ValueError("bad point cloud shape") @@ -32,165 +36,101 @@ def check_shape(self, points): if (len(points.shape) != 2) or (points.shape[1] not in (2,3)): raise ValueError("Point cloud positions should have shape (N,3); shape is " + str(points.shape)) - def n_points(self): - return self.bound_cloud.n_points() + return self.bound_instance.n_points() - ## Structure management - - def remove(self): - '''Remove the structure itself''' - self.bound_cloud.remove() - def remove_all_quantities(self): - '''Remove all quantities on the structure''' - self.bound_cloud.remove_all_quantities() - def remove_quantity(self, name): - '''Remove a single quantity on the structure''' - self.bound_cloud.remove_quantity(name) - - # Enable/disable - def set_enabled(self, val=True): - self.bound_cloud.set_enabled(val) - def is_enabled(self): - return self.bound_cloud.is_enabled() - - # Transparency - def set_transparency(self, val): - self.bound_cloud.set_transparency(val) - def get_transparency(self): - return self.bound_cloud.get_transparency() - - # Transformation things - def center_bounding_box(self): - self.bound_cloud.center_bounding_box() - def rescale_to_unit(self): - self.bound_cloud.rescale_to_unit() - def reset_transform(self): - self.bound_cloud.reset_transform() - def set_transform(self, new_mat4x4): - self.bound_cloud.set_transform(new_mat4x4) - def set_position(self, new_vec3): - self.bound_cloud.set_position(new_vec3) - def translate(self, trans_vec3): - self.bound_cloud.translate(trans_vec3) - def get_transform(self): - return self.bound_cloud.get_transform() - def get_position(self): - return self.bound_cloud.get_position() - # Point render mode def set_point_render_mode(self, val): - self.bound_cloud.set_point_render_mode(str_to_point_render_mode(val)) + self.bound_instance.set_point_render_mode(str_to_point_render_mode(val)) def get_point_render_mode(self): - return point_render_mode_to_str(self.bound_cloud.get_point_render_mode()) - - # Slice planes - def set_cull_whole_elements(self, val): - self.bound_cloud.set_cull_whole_elements(val) - def get_cull_whole_elements(self): - return self.bound_cloud.get_cull_whole_elements() - def set_ignore_slice_plane(self, plane, val): - # take either a string or a slice plane object as input - if isinstance(plane, str): - self.bound_cloud.set_ignore_slice_plane(plane, val) - else: - self.bound_cloud.set_ignore_slice_plane(plane.get_name(), val) - def get_ignore_slice_plane(self, plane): - # take either a string or a slice plane object as input - if isinstance(plane, str): - return self.bound_cloud.get_ignore_slice_plane(plane) - else: - return self.bound_cloud.get_ignore_slice_plane(plane.get_name()) + return point_render_mode_to_str(self.bound_instance.get_point_render_mode()) + # Update def update_point_positions(self, points): self.check_shape(points) if points.shape[1] == 3: - self.bound_cloud.update_point_positions(points) + self.bound_instance.update_point_positions(points) elif points.shape[1] == 2: - self.bound_cloud.update_point_positions2D(points) + self.bound_instance.update_point_positions2D(points) else: raise ValueError("bad point cloud shape") def set_point_radius_quantity(self, quantity_name, autoscale=True): - self.bound_cloud.set_point_radius_quantity(quantity_name, autoscale) + self.bound_instance.set_point_radius_quantity(quantity_name, autoscale) def clear_point_radius_quantity(self): - self.bound_cloud.clear_point_radius_quantity() + self.bound_instance.clear_point_radius_quantity() ## Options # Point radius def set_radius(self, rad, relative=True): - self.bound_cloud.set_radius(rad, relative) + self.bound_instance.set_radius(rad, relative) def get_radius(self): - return self.bound_cloud.get_radius() + return self.bound_instance.get_radius() # Point color def set_color(self, val): - self.bound_cloud.set_color(glm3(val)) + self.bound_instance.set_color(glm3(val)) def get_color(self): - return self.bound_cloud.get_color().as_tuple() + return self.bound_instance.get_color().as_tuple() # Point material def set_material(self, mat): - self.bound_cloud.set_material(mat) + self.bound_instance.set_material(mat) def get_material(self): - return self.bound_cloud.get_material() + return self.bound_instance.get_material() ## Quantities # Scalar - def add_scalar_quantity(self, name, values, enabled=None, datatype="standard", vminmax=None, cmap=None): + def add_scalar_quantity(self, name, values, datatype="standard", **scalar_args): if len(values.shape) != 1 or values.shape[0] != self.n_points(): raise ValueError("'values' should be a length-N array") - q = self.bound_cloud.add_scalar_quantity(name, values, str_to_datatype(datatype)) - - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if vminmax is not None: - q.set_map_range(vminmax) - if cmap is not None: - q.set_color_map(cmap) + q = self.bound_instance.add_scalar_quantity(name, values, str_to_datatype(datatype)) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, scalar_args) + process_scalar_args(self, q, scalar_args) + check_all_args_processed(self, q, scalar_args) # Color - def add_color_quantity(self, name, values, enabled=None): + def add_color_quantity(self, name, values, **color_args): if len(values.shape) != 2 or values.shape[0] != self.n_points() or values.shape[1] != 3: raise ValueError("'values' should be an Nx3 array") - q = self.bound_cloud.add_color_quantity(name, values) - - # Support optional params - if enabled is not None: - q.set_enabled(enabled) + q = self.bound_instance.add_color_quantity(name, values) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, color_args) + process_color_args(self, q, color_args) + check_all_args_processed(self, q, color_args) # Vector - def add_vector_quantity(self, name, values, enabled=None, vectortype="standard", length=None, radius=None, color=None): + def add_vector_quantity(self, name, values, vectortype="standard", **vector_args): if len(values.shape) != 2 or values.shape[0] != self.n_points() or values.shape[1] not in [2,3]: raise ValueError("'values' should be an Nx3 array (or Nx2 for 2D)") if values.shape[1] == 2: - q = self.bound_cloud.add_vector_quantity2D(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_vector_quantity2D(name, values, str_to_vectortype(vectortype)) elif values.shape[1] == 3: - q = self.bound_cloud.add_vector_quantity(name, values, str_to_vectortype(vectortype)) - - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if length is not None: - q.set_length(length, True) - if radius is not None: - q.set_radius(radius, True) - if color is not None: - q.set_color(glm3(color)) + q = self.bound_instance.add_vector_quantity(name, values, str_to_vectortype(vectortype)) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, vector_args) + process_vector_args(self, q, vector_args) + check_all_args_processed(self, q, vector_args) def register_point_cloud(name, points, enabled=None, radius=None, point_render_mode=None, color=None, material=None, transparency=None): """Register a new point cloud""" - if not psb.isInitialized(): + if not psb.is_initialized(): raise RuntimeError("Polyscope has not been initialized") p = PointCloud(name, points) diff --git a/src/polyscope/structure.py b/src/polyscope/structure.py new file mode 100644 index 0000000..d80742f --- /dev/null +++ b/src/polyscope/structure.py @@ -0,0 +1,208 @@ +import polyscope_bindings as psb + +from polyscope.floating_quantities import add_scalar_image_quantity, add_color_image_quantity, add_color_alpha_image_quantity, add_depth_render_image_quantity, add_color_render_image_quantity, add_scalar_render_image_quantity, add_raw_color_render_image_quantity, add_raw_color_alpha_render_image_quantity +from polyscope.managed_buffer import ManagedBuffer + +# Base class for common properties and methods on structures +class Structure: + + def __init__(self): + self.bound_instance = None + + + ## Structure management + + def get_name(self): + '''Get the name''' + return self.bound_instance.get_name() + def remove(self): + '''Remove the structure itself''' + self.bound_instance.remove() + def remove_all_quantities(self): + '''Remove all quantities on the structure''' + self.bound_instance.remove_all_quantities() + def remove_quantity(self, name): + '''Remove a single quantity on the structure''' + self.bound_instance.remove_quantity(name) + + + ## Enable/disable + + def set_enabled(self, val=True): + self.bound_instance.set_enabled(val) + def is_enabled(self): + return self.bound_instance.is_enabled() + + ## Transparency + + def set_transparency(self, val): + self.bound_instance.set_transparency(val) + def get_transparency(self): + return self.bound_instance.get_transparency() + + ## Transformation things + + def center_bounding_box(self): + self.bound_instance.center_bounding_box() + def rescale_to_unit(self): + self.bound_instance.rescale_to_unit() + def reset_transform(self): + self.bound_instance.reset_transform() + def set_transform(self, new_mat4x4): + self.bound_instance.set_transform(new_mat4x4) + def set_position(self, new_vec3): + self.bound_instance.set_position(new_vec3) + def translate(self, trans_vec3): + self.bound_instance.translate(trans_vec3) + def get_transform(self): + return self.bound_instance.get_transform() + def get_position(self): + return self.bound_instance.get_position() + + ## Managed Buffers + + def get_buffer(self, buffer_name): + + present, buffer_type = self.bound_instance.has_buffer_type(buffer_name) + + if not present: raise ValueError("structure has no buffer named " + buffer_name) + + return { + psb.ManagedBufferType.Float : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Float (n), t), + psb.ManagedBufferType.Double : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Double (n), t), + psb.ManagedBufferType.Vec2 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Vec2 (n), t), + psb.ManagedBufferType.Vec3 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Vec3 (n), t), + psb.ManagedBufferType.Vec4 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Vec4 (n), t), + psb.ManagedBufferType.Arr2Vec3 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Arr2Vec4(n), t), + psb.ManagedBufferType.Arr3Vec3 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Arr3Vec4(n), t), + psb.ManagedBufferType.Arr4Vec3 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Arr4Vec4(n), t), + psb.ManagedBufferType.UInt32 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_UInt32 (n), t), + psb.ManagedBufferType.Int32 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_Int32 (n), t), + psb.ManagedBufferType.UVec2 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_UVec2 (n), t), + psb.ManagedBufferType.UVec3 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_UVec3 (n), t), + psb.ManagedBufferType.UVec4 : lambda n,t : ManagedBuffer(self.bound_instance.get_buffer_UVec4 (n), t), + }[buffer_type](buffer_name, buffer_type) + + def get_quantity_buffer(self, quantity_name, buffer_name): + + present, buffer_type = self.bound_instance.has_quantity_buffer_type(quantity_name, buffer_name) + + if not present: + if self.get_name() == psb.get_global_floating_quantity_structure().get_name(): + # give a more informative error if this was called on the global floating quantity, becuase it is particularly easy for users to get confused and call this function at the global scope rather than on a quantity + raise ValueError(f"There is no quantity {quantity_name} with buffer named {buffer_name}. NOTE: calling polyscope.get_quantity_buffer() is for global floating quantities only, call structure.get_quantity_buffer() to get buffers for a quantity added to some structure.") + else: + raise ValueError(f"Structure {self.get_name()} does not have a quantity {quantity_name} with buffer named {buffer_name}") + + return { + psb.ManagedBufferType.Float : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Float (q,n), t), + psb.ManagedBufferType.Double : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Double (q,n), t), + psb.ManagedBufferType.Vec2 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Vec2 (q,n), t), + psb.ManagedBufferType.Vec3 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Vec3 (q,n), t), + psb.ManagedBufferType.Vec4 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Vec4 (q,n), t), + psb.ManagedBufferType.Arr2Vec3 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Arr2Vec4(q,n), t), + psb.ManagedBufferType.Arr3Vec3 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Arr3Vec4(q,n), t), + psb.ManagedBufferType.Arr4Vec3 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Arr4Vec4(q,n), t), + psb.ManagedBufferType.UInt32 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_UInt32 (q,n), t), + psb.ManagedBufferType.Int32 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_Int32 (q,n), t), + psb.ManagedBufferType.UVec2 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_UVec2 (q,n), t), + psb.ManagedBufferType.UVec3 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_UVec3 (q,n), t), + psb.ManagedBufferType.UVec4 : lambda q,n,t : ManagedBuffer(self.bound_instance.get_quantity_buffer_UVec4 (q,n), t), + }[buffer_type](quantity_name, buffer_name, buffer_type) + + ## Groups + def add_to_group(self, group): + # take either a string or a group object as input + if isinstance(group, str): + self.bound_instance.add_to_group(group) + else: + self.bound_instance.add_to_group(group.get_name()) + + ## Slice planes + + def set_cull_whole_elements(self, val): + self.bound_instance.set_cull_whole_elements(val) + def get_cull_whole_elements(self): + return self.bound_instance.get_cull_whole_elements() + def set_ignore_slice_plane(self, plane, val): + # take either a string or a slice plane object as input + if isinstance(plane, str): + self.bound_instance.set_ignore_slice_plane(plane, val) + else: + self.bound_instance.set_ignore_slice_plane(plane.get_name(), val) + def get_ignore_slice_plane(self, plane): + # take either a string or a slice plane object as input + if isinstance(plane, str): + return self.bound_instance.get_ignore_slice_plane(plane) + else: + return self.bound_instance.get_ignore_slice_plane(plane.get_name()) + + + ## Image Floating Quantities + + def add_scalar_image_quantity(self, name, values, image_origin="upper_left", datatype="standard", **option_args): + """ + Add a "floating" image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_scalar_image_quantity(name, values, image_origin=image_origin, datatype=datatype, struct_ref=self, **option_args) + + def add_color_image_quantity(self, name, values, image_origin="upper_left", **option_args): + """ + Add a "floating" image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_color_image_quantity(name, values, image_origin=image_origin, struct_ref=self, **option_args) + + def add_color_alpha_image_quantity(self, name, values, image_origin="upper_left", **option_args): + """ + Add a "floating" image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_color_alpha_image_quantity(name, values, image_origin=image_origin, struct_ref=self, **option_args) + + ## Render Image + + def add_depth_render_image_quantity(self, name, depth_values, normal_values, image_origin="upper_left", color=None, **option_args): + """ + Add a "floating" render image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_depth_render_image_quantity(name, depth_values, normal_values, image_origin=image_origin, color=color, struct_ref=self, **option_args) + + def add_color_render_image_quantity(self, name, depth_values, normal_values, color_values, image_origin="upper_left", **option_args): + """ + Add a "floating" render image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_color_render_image_quantity(name, depth_values, normal_values, color_values, image_origin=image_origin, struct_ref=self, **option_args) + + def add_scalar_render_image_quantity(self, name, depth_values, normal_values, scalar_values, image_origin="upper_left", **option_args): + """ + Add a "floating" render image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_scalar_render_image_quantity(name, depth_values, normal_values, scalar_values, image_origin=image_origin, struct_ref=self, **option_args) + + def add_raw_color_render_image_quantity(self, name, depth_values, color_values, image_origin="upper_left", **option_args): + """ + Add a "floating" render image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_raw_color_render_image_quantity(name, depth_values, color_values, image_origin=image_origin, struct_ref=self, **option_args) + + def add_raw_color_alpha_render_image_quantity(self, name, depth_values, color_values, image_origin="upper_left", **option_args): + """ + Add a "floating" render image quantity to the structure + """ + + # Call the general version (this abstraction allows us to handle the free-floating case via the same code) + return add_raw_color_alpha_render_image_quantity(name, depth_values, color_values, image_origin=image_origin, struct_ref=self, **option_args) + diff --git a/src/polyscope/surface_mesh.py b/src/polyscope/surface_mesh.py index 173c9ef..1badab0 100644 --- a/src/polyscope/surface_mesh.py +++ b/src/polyscope/surface_mesh.py @@ -3,18 +3,22 @@ from polyscope.core import str_to_datatype, str_to_vectortype, str_to_param_coords_type, \ str_to_param_viz_style, str_to_back_face_policy, back_face_policy_to_str,\ - glm3 + str_to_image_origin, glm3 +from polyscope.structure import Structure +from polyscope.common import process_quantity_args, process_scalar_args, process_color_args, process_vector_args, process_parameterization_args, check_all_args_processed, check_is_scalar_image, check_is_image3 -class SurfaceMesh: +class SurfaceMesh(Structure): # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope # End users should not call this constrctor, use register_surface_mesh instead def __init__(self, name=None, vertices=None, faces=None, instance=None): + + super().__init__() if instance is not None: # Wrap an existing instance - self.bound_mesh = instance + self.bound_instance = instance else: # Create a new instance @@ -26,9 +30,9 @@ def __init__(self, name=None, vertices=None, faces=None, instance=None): if (len(faces.shape) != 2): raise ValueError("surface mesh face array should have shape (F,D) for some D; shape is " + str(faces.shape)) if vertices.shape[1] == 3: - self.bound_mesh = psb.register_surface_mesh(name, vertices, faces) + self.bound_instance = psb.register_surface_mesh(name, vertices, faces) elif vertices.shape[1] == 2: - self.bound_mesh = psb.register_surface_mesh2D(name, vertices, faces) + self.bound_instance = psb.register_surface_mesh2D(name, vertices, faces) else: # Faces is something else, try to iterate through it to build a list of lists @@ -38,9 +42,9 @@ def __init__(self, name=None, vertices=None, faces=None, instance=None): faces_copy.append(f_copy) if vertices.shape[1] == 3: - self.bound_mesh = psb.register_surface_mesh_list(name, vertices, faces) + self.bound_instance = psb.register_surface_mesh_list(name, vertices, faces) elif vertices.shape[1] == 2: - self.bound_mesh = psb.register_surface_mesh_list2D(name, vertices, faces) + self.bound_instance = psb.register_surface_mesh_list2D(name, vertices, faces) @@ -51,85 +55,25 @@ def check_shape(self, points): def n_vertices(self): - return self.bound_mesh.n_vertices() + return self.bound_instance.n_vertices() def n_faces(self): - return self.bound_mesh.n_faces() + return self.bound_instance.n_faces() def n_edges(self): - return self.bound_mesh.n_edges() + return self.bound_instance.n_edges() def n_halfedges(self): - return self.bound_mesh.n_halfedges() + return self.bound_instance.n_halfedges() def n_corners(self): - return self.bound_mesh.n_corners() + return self.bound_instance.n_corners() - ## Structure management - - def remove(self): - '''Remove the structure itself''' - self.bound_mesh.remove() - def remove_all_quantities(self): - '''Remove all quantities on the structure''' - self.bound_mesh.remove_all_quantities() - def remove_quantity(self, name): - '''Remove a single quantity on the structure''' - self.bound_mesh.remove_quantity(name) - - # Enable/disable - def set_enabled(self, val=True): - self.bound_mesh.set_enabled(val) - def is_enabled(self): - return self.bound_mesh.is_enabled() - - # Transparency - def set_transparency(self, val): - self.bound_mesh.set_transparency(val) - def get_transparency(self): - return self.bound_mesh.get_transparency() - - # Transformation things - def center_bounding_box(self): - self.bound_mesh.center_bounding_box() - def rescale_to_unit(self): - self.bound_mesh.rescale_to_unit() - def reset_transform(self): - self.bound_mesh.reset_transform() - def set_transform(self, new_mat4x4): - self.bound_mesh.set_transform(new_mat4x4) - def set_position(self, new_vec3): - self.bound_mesh.set_position(new_vec3) - def translate(self, trans_vec3): - self.bound_mesh.translate(trans_vec3) - def get_transform(self): - return self.bound_mesh.get_transform() - def get_position(self): - return self.bound_mesh.get_position() - - - # Slice planes - def set_cull_whole_elements(self, val): - self.bound_mesh.set_cull_whole_elements(val) - def get_cull_whole_elements(self): - return self.bound_mesh.get_cull_whole_elements() - def set_ignore_slice_plane(self, plane, val): - # take either a string or a slice plane object as input - if isinstance(plane, str): - self.bound_mesh.set_ignore_slice_plane(plane, val) - else: - self.bound_mesh.set_ignore_slice_plane(plane.get_name(), val) - def get_ignore_slice_plane(self, plane): - # take either a string or a slice plane object as input - if isinstance(plane, str): - return self.bound_mesh.get_ignore_slice_plane(plane) - else: - return self.bound_mesh.get_ignore_slice_plane(plane.get_name()) # Update def update_vertex_positions(self, vertices): self.check_shape(vertices) if vertices.shape[1] == 3: - self.bound_mesh.update_vertex_positions(vertices) + self.bound_instance.update_vertex_positions(vertices) elif vertices.shape[1] == 2: - self.bound_mesh.update_vertex_positions2D(vertices) + self.bound_instance.update_vertex_positions2D(vertices) else: raise ValueError("bad vertex shape") @@ -138,158 +82,153 @@ def update_vertex_positions(self, vertices): # Color def set_color(self, val): - self.bound_mesh.set_color(glm3(val)) + self.bound_instance.set_color(glm3(val)) def get_color(self): - return self.bound_mesh.get_color().as_tuple() + return self.bound_instance.get_color().as_tuple() # Edge Color def set_edge_color(self, val): - self.bound_mesh.set_edge_color(glm3(val)) + self.bound_instance.set_edge_color(glm3(val)) def get_edge_color(self): - return self.bound_mesh.get_edge_color().as_tuple() + return self.bound_instance.get_edge_color().as_tuple() # Edge width def set_edge_width(self, val): - self.bound_mesh.set_edge_width(val) + self.bound_instance.set_edge_width(val) def get_edge_width(self): - return self.bound_mesh.get_edge_width() + return self.bound_instance.get_edge_width() # Smooth shade def set_smooth_shade(self, val): - self.bound_mesh.set_smooth_shade(val) + self.bound_instance.set_smooth_shade(val) def get_smooth_shade(self): - return self.bound_mesh.get_smooth_shade() + return self.bound_instance.get_smooth_shade() # Material def set_material(self, mat): - self.bound_mesh.set_material(mat) + self.bound_instance.set_material(mat) def get_material(self): - return self.bound_mesh.get_material() + return self.bound_instance.get_material() # Color def set_back_face_policy(self, val): - self.bound_mesh.set_back_face_policy(str_to_back_face_policy(val)) + self.bound_instance.set_back_face_policy(str_to_back_face_policy(val)) def get_back_face_policy(self): - return back_face_policy_to_str(self.bound_mesh.get_back_face_policy()) + return back_face_policy_to_str(self.bound_instance.get_back_face_policy()) # Back face color def set_back_face_color(self, val): - self.bound_mesh.set_back_face_color(glm3(val)) + self.bound_instance.set_back_face_color(glm3(val)) def get_back_face_color(self): - return self.bound_mesh.get_back_face_color().as_tuple() + return self.bound_instance.get_back_face_color().as_tuple() + - ## Permutations and bases + def mark_edges_as_used(self): + self.bound_instance.mark_edges_as_used() + + def mark_halfedges_as_used(self): + self.bound_instance.mark_halfedges_as_used() + + def mark_corners_as_used(self): + self.bound_instance.mark_corners_as_used() - def set_vertex_permutation(self, perm, expected_size=None): - if len(perm.shape) != 1 or perm.shape[0] != self.n_vertices(): raise ValueError("'perm' should be an array with one entry per vertex") - if expected_size is None: expected_size = 0 - self.bound_mesh.set_vertex_permutation(perm, expected_size) + ## Permutations and bases - def set_face_permutation(self, perm, expected_size=None): - if len(perm.shape) != 1 or perm.shape[0] != self.n_faces(): raise ValueError("'perm' should be an array with one entry per face") - if expected_size is None: expected_size = 0 - self.bound_mesh.set_face_permutation(perm, expected_size) - def set_edge_permutation(self, perm, expected_size=None): if len(perm.shape) != 1 or perm.shape[0] != self.n_edges(): raise ValueError("'perm' should be an array with one entry per edge") if expected_size is None: expected_size = 0 - self.bound_mesh.set_edge_permutation(perm, expected_size) + self.bound_instance.set_edge_permutation(perm, expected_size) def set_corner_permutation(self, perm, expected_size=None): if len(perm.shape) != 1 or perm.shape[0] != self.n_corners(): raise ValueError("'perm' should be an array with one entry per corner") if expected_size is None: expected_size = 0 - self.bound_mesh.set_corner_permutation(perm, expected_size) + self.bound_instance.set_corner_permutation(perm, expected_size) def set_halfedge_permutation(self, perm, expected_size=None): if len(perm.shape) != 1 or perm.shape[0] != self.n_halfedges(): raise ValueError("'perm' should be an array with one entry per halfedge") if expected_size is None: expected_size = 0 - self.bound_mesh.set_halfedge_permutation(perm, expected_size) + self.bound_instance.set_halfedge_permutation(perm, expected_size) def set_all_permutations(self, - vertex_perm=None, vertex_perm_size=None, - face_perm=None, face_perm_size=None, + vertex_perm=None, vertex_perm_size=None, # now ignored + face_perm=None, face_perm_size=None, # now ignored edge_perm=None, edge_perm_size=None, corner_perm=None, corner_perm_size=None, halfedge_perm=None, halfedge_perm_size=None): - if vertex_perm is not None: self.set_vertex_permutation(vertex_perm, vertex_perm_size) - if face_perm is not None: self.set_face_permutation(face_perm, face_perm_size) if edge_perm is not None: self.set_edge_permutation(edge_perm, edge_perm_size) if corner_perm is not None: self.set_corner_permutation(corner_perm, corner_perm_size) if halfedge_perm is not None: self.set_halfedge_permutation(halfedge_perm, halfedge_perm_size) - def set_vertex_tangent_basisX(self, vectors): - if len(vectors.shape) != 2 or vectors.shape[0] != self.n_vertices() or vectors.shape[1] not in (2,3): - raise ValueError("'vectors' should be an array with one entry per vertex") - - if vectors.shape[1] == 2: - self.bound_mesh.set_vertex_tangent_basisX2D(vectors) - elif vectors.shape[1] == 3: - self.bound_mesh.set_vertex_tangent_basisX(vectors) - - def set_face_tangent_basisX(self, vectors): - if len(vectors.shape) != 2 or vectors.shape[0] != self.n_faces() or vectors.shape[1] not in (2,3): - raise ValueError("'vectors' should be an array with one entry per face") - - if vectors.shape[1] == 2: - self.bound_mesh.set_face_tangent_basisX2D(vectors) - elif vectors.shape[1] == 3: - self.bound_mesh.set_face_tangent_basisX(vectors) - - ## Quantities # Scalar - def add_scalar_quantity(self, name, values, defined_on='vertices', enabled=None, datatype="standard", vminmax=None, cmap=None): + def add_scalar_quantity(self, name, values, defined_on='vertices', datatype="standard", param_name=None, image_origin="upper_left", **scalar_args): - if len(values.shape) != 1: raise ValueError("'values' should be a length-N array") + if defined_on != 'texture' and len(values.shape) != 1: raise ValueError("'values' should be a length-N array") if defined_on == 'vertices': if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") - q = self.bound_mesh.add_vertex_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_vertex_scalar_quantity(name, values, str_to_datatype(datatype)) elif defined_on == 'faces': if values.shape[0] != self.n_faces(): raise ValueError("'values' should be a length n_faces array") - q = self.bound_mesh.add_face_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_face_scalar_quantity(name, values, str_to_datatype(datatype)) elif defined_on == 'edges': if values.shape[0] != self.n_edges(): raise ValueError("'values' should be a length n_edges array") - q = self.bound_mesh.add_edge_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_edge_scalar_quantity(name, values, str_to_datatype(datatype)) elif defined_on == 'halfedges': if values.shape[0] != self.n_halfedges(): raise ValueError("'values' should be a length n_halfedges array") - q = self.bound_mesh.add_halfedge_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_halfedge_scalar_quantity(name, values, str_to_datatype(datatype)) + elif defined_on == 'texture': + check_is_scalar_image(values) + dimY = values.shape[0] + dimX = values.shape[1] + if not isinstance(param_name, str): + raise ValueError("when adding a quantity defined in a texture, you must pass 'param_name' as a string giving the name of a parameterization quantity on this structure, which provides the UV coords") + q = self.bound_instance.add_texture_scalar_quantity(name, param_name, dimX, dimY, values.flatten(), str_to_image_origin(image_origin), str_to_datatype(datatype)) else: - raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'faces', 'edges', 'halfedges']".format(defined_on)) + raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'faces', 'edges', 'halfedges', 'texture']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if vminmax is not None: - q.set_map_range(vminmax) - if cmap is not None: - q.set_color_map(cmap) - + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, scalar_args) + process_scalar_args(self, q, scalar_args) + check_all_args_processed(self, q, scalar_args) + # Color - def add_color_quantity(self, name, values, defined_on='vertices', enabled=None): - if len(values.shape) != 2 or values.shape[1] != 3: raise ValueError("'values' should be an Nx3 array") + def add_color_quantity(self, name, values, defined_on='vertices', param_name=None, image_origin="upper_left", **color_args): + if defined_on != 'texture' and (len(values.shape) != 2 or values.shape[1] != 3): raise ValueError("'values' should be an Nx3 array") if defined_on == 'vertices': if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") - q = self.bound_mesh.add_vertex_color_quantity(name, values) + q = self.bound_instance.add_vertex_color_quantity(name, values) elif defined_on == 'faces': if values.shape[0] != self.n_faces(): raise ValueError("'values' should be a length n_faces array") - q = self.bound_mesh.add_face_color_quantity(name, values) + q = self.bound_instance.add_face_color_quantity(name, values) + elif defined_on == 'texture': + check_is_image3(values) + dimY = values.shape[0] + dimX = values.shape[1] + if not isinstance(param_name, str): + raise ValueError("when adding a quantity defined in a texture, you must pass 'param_name' as a string giving the name of a parameterization quantity on this structure, which provides the UV coords") + q = self.bound_instance.add_texture_color_quantity(name, param_name, dimX, dimY, values.reshape(-1,3), str_to_image_origin(image_origin)) else: - raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'faces']".format(defined_on)) + raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'faces', 'texture']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, color_args) + process_color_args(self, q, color_args) + check_all_args_processed(self, q, color_args) # Distance + # [deprecated], this is just a special set of options for a scalar quantity now def add_distance_quantity(self, name, values, defined_on='vertices', enabled=None, signed=False, vminmax=None, stripe_size=None, stripe_size_relative=True, cmap=None): if len(values.shape) != 1: raise ValueError("'values' should be a length-N array") @@ -298,9 +237,9 @@ def add_distance_quantity(self, name, values, defined_on='vertices', enabled=Non if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") if signed: - q = self.bound_mesh.add_vertex_signed_distance_quantity(name, values) + q = self.bound_instance.add_vertex_signed_distance_quantity(name, values) else: - q = self.bound_mesh.add_vertex_distance_quantity(name, values) + q = self.bound_instance.add_vertex_distance_quantity(name, values) else: raise ValueError("bad `defined_on` value {}, should be one of ['vertices']".format(defined_on)) @@ -317,7 +256,7 @@ def add_distance_quantity(self, name, values, defined_on='vertices', enabled=Non # Parameterization - def add_parameterization_quantity(self, name, values, defined_on='vertices', coords_type='unit', enabled=None, viz_style=None, grid_colors=None, checker_colors=None, checker_size=None, cmap=None): + def add_parameterization_quantity(self, name, values, defined_on='vertices', coords_type='unit', **parameterization_args): if len(values.shape) != 2 or values.shape[1] != 2: raise ValueError("'values' should be an (Nx2) array") @@ -326,32 +265,22 @@ def add_parameterization_quantity(self, name, values, defined_on='vertices', coo if defined_on == 'vertices': if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") - q = self.bound_mesh.add_vertex_parameterization_quantity(name, values, coords_type_enum) + q = self.bound_instance.add_vertex_parameterization_quantity(name, values, coords_type_enum) elif defined_on == 'corners': if values.shape[0] != self.n_corners(): raise ValueError("'values' should be a length n_faces array") - q = self.bound_mesh.add_corner_parameterization_quantity(name, values, coords_type_enum) + q = self.bound_instance.add_corner_parameterization_quantity(name, values, coords_type_enum) else: raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'corners']".format(defined_on)) - - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if viz_style is not None: - viz_style_enum = str_to_param_viz_style(viz_style) - q.set_style(viz_style_enum) - if grid_colors is not None: - q.set_grid_colors((glm3(grid_colors[0]), glm3(grid_colors[1]))) - if checker_colors is not None: - q.set_checker_colors((glm3(checker_colors[0]), glm3(checker_colors[1]))) - if checker_size is not None: - q.set_checker_size(checker_size) - if cmap is not None: - q.set_color_map(cmap) + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, parameterization_args) + process_parameterization_args(self, q, parameterization_args) + check_all_args_processed(self, q, parameterization_args) # Vector - def add_vector_quantity(self, name, values, defined_on='vertices', enabled=None, vectortype="standard", length=None, radius=None, color=None): + def add_vector_quantity(self, name, values, defined_on='vertices', vectortype="standard", **vector_args): if len(values.shape) != 2 or values.shape[1] not in [2,3]: raise ValueError("'values' should be an Nx3 array (or Nx2 for 2D)") @@ -359,88 +288,81 @@ def add_vector_quantity(self, name, values, defined_on='vertices', enabled=None, if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") if values.shape[1] == 2: - q = self.bound_mesh.add_vertex_vector_quantity2D(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_vertex_vector_quantity2D(name, values, str_to_vectortype(vectortype)) elif values.shape[1] == 3: - q = self.bound_mesh.add_vertex_vector_quantity(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_vertex_vector_quantity(name, values, str_to_vectortype(vectortype)) elif defined_on == 'faces': if values.shape[0] != self.n_faces(): raise ValueError("'values' should be a length n_faces array") if values.shape[1] == 2: - q = self.bound_mesh.add_face_vector_quantity2D(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_face_vector_quantity2D(name, values, str_to_vectortype(vectortype)) elif values.shape[1] == 3: - q = self.bound_mesh.add_face_vector_quantity(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_face_vector_quantity(name, values, str_to_vectortype(vectortype)) else: raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'faces']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if length is not None: - q.set_length(length, True) - if radius is not None: - q.set_radius(radius, True) - if color is not None: - q.set_color(glm3(color)) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, vector_args) + process_vector_args(self, q, vector_args) + check_all_args_processed(self, q, vector_args) - def add_intrinsic_vector_quantity(self, name, values, n_sym=1, defined_on='vertices', enabled=None, vectortype="standard", length=None, radius=None, color=None, ribbon=None): + def add_tangent_vector_quantity(self, name, values, basisX, basisY, n_sym=1, defined_on='vertices', vectortype="standard", **vector_args): if len(values.shape) != 2 or values.shape[1] != 2: raise ValueError("'values' should be an Nx2 array") + if len(basisX.shape) != 2 or basisX.shape[1] != 3: raise ValueError("'basisX' should be an Nx3 array") + if len(basisY.shape) != 2 or basisY.shape[1] != 3: raise ValueError("'basisY' should be an Nx3 array") if defined_on == 'vertices': if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") + if basisX.shape[0] != self.n_vertices(): raise ValueError("'basisX' should be a length n_vertices array") + if basisY.shape[0] != self.n_vertices(): raise ValueError("'basisY' should be a length n_vertices array") - q = self.bound_mesh.add_vertex_intrinsic_vector_quantity(name, values, n_sym, str_to_vectortype(vectortype)) + q = self.bound_instance.add_vertex_tangent_vector_quantity(name, values, basisX, basisY, n_sym, str_to_vectortype(vectortype)) elif defined_on == 'faces': if values.shape[0] != self.n_faces(): raise ValueError("'values' should be a length n_faces array") + if basisX.shape[0] != self.n_faces(): raise ValueError("'basisX' should be a length n_faces array") + if basisY.shape[0] != self.n_faces(): raise ValueError("'basisY' should be a length n_faces array") - q = self.bound_mesh.add_face_intrinsic_vector_quantity(name, values, n_sym, str_to_vectortype(vectortype)) + q = self.bound_instance.add_face_tangent_vector_quantity(name, values, basisX, basisY, n_sym, str_to_vectortype(vectortype)) else: raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'faces']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if length is not None: - q.set_length(length, True) - if radius is not None: - q.set_radius(radius, True) - if color is not None: - q.set_color(glm3(color)) - if ribbon is not None: - q.set_ribbon_enabled(ribbon) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, vector_args) + process_vector_args(self, q, vector_args) + check_all_args_processed(self, q, vector_args) - def add_one_form_vector_quantity(self, name, values, orientations, enabled=None, length=None, radius=None, color=None, ribbon=None): + def add_one_form_vector_quantity(self, name, values, orientations, **vector_args): if len(values.shape) != 1 or values.shape[0] != self.n_edges(): raise ValueError("'values' should be length n_edges array") if len(orientations.shape) != 1 or orientations.shape[0] != self.n_edges(): raise ValueError("'orientations' should be length n_edges array") - q = self.bound_mesh.add_one_form_intrinsic_vector_quantity(name, values, orientations) + q = self.bound_instance.add_one_form_tangent_vector_quantity(name, values, orientations) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, vector_args) + process_vector_args(self, q, vector_args) + check_all_args_processed(self, q, vector_args) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if length is not None: - q.set_length(length, True) - if radius is not None: - q.set_radius(radius, True) - if color is not None: - q.set_color(glm3(color)) - if ribbon is not None: - q.set_ribbon_enabled(ribbon) def register_surface_mesh(name, vertices, faces, enabled=None, color=None, edge_color=None, smooth_shade=None, edge_width=None, material=None, back_face_policy=None, back_face_color=None, transparency=None): """Register a new surface mesh""" - if not psb.isInitialized(): + if not psb.is_initialized(): raise RuntimeError("Polyscope has not been initialized") p = SurfaceMesh(name, vertices, faces) diff --git a/src/polyscope/volume_grid.py b/src/polyscope/volume_grid.py new file mode 100644 index 0000000..ef7ce7a --- /dev/null +++ b/src/polyscope/volume_grid.py @@ -0,0 +1,216 @@ +import polyscope_bindings as psb +import numpy as np + +from polyscope.core import str_to_datatype, str_to_vectortype, str_to_param_coords_type, str_to_param_viz_style, glm3, glm3u +from polyscope.structure import Structure +from polyscope.common import process_quantity_args, process_scalar_args, process_color_args, process_vector_args, check_all_args_processed, check_and_pop_arg + +def process_volume_grid_scalar_args(structure, quantity, scalar_args, defined_on): + + val = check_and_pop_arg(scalar_args, 'enable_gridcube_viz') + if val is not None: + quantity.set_gridcube_viz_enabled(val) + + if defined_on == 'nodes': + + val = check_and_pop_arg(scalar_args, 'enable_isosurface_viz') + if val is not None: + quantity.set_isosurface_viz_enabled(val) + + val = check_and_pop_arg(scalar_args, 'isosurface_level') + if val is not None: + quantity.set_isosurface_level(val) + + val = check_and_pop_arg(scalar_args, 'isosurface_color') + if val is not None: + quantity.set_isosurface_color(glm3(val)) + + val = check_and_pop_arg(scalar_args, 'slice_planes_affect_isosurface') + if val is not None: + quantity.set_slice_planes_affect_isosurface(val) + + val = check_and_pop_arg(scalar_args, 'register_isosurface_as_mesh_with_name') + if val is not None: + quantity.register_isosurface_as_mesh_with_name(val) + + +class VolumeGrid(Structure): + + # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope + + # End users should not call this constrctor, use register_volume_grid instead + def __init__(self, name=None, node_dims=None, bound_low=None, bound_high=None, instance=None): + + super().__init__() + + if instance is not None: + # Wrap an existing instance + self.bound_instance = instance + + else: + # Create a new instance + + node_dims = glm3u(node_dims) + bound_low = glm3(bound_low) + bound_high = glm3(bound_high) + + self.bound_instance = psb.register_volume_grid(name, node_dims, bound_low, bound_high) + + + def n_nodes(self): + return self.bound_instance.n_nodes() + def n_cells(self): + return self.bound_instance.n_cells() + def grid_spacing(self): + return self.bound_instance.grid_spacing() + def get_grid_node_dim(self): + return self.bound_instance.get_grid_node_dim().as_tuple() + def get_grid_cell_dim(self): + return self.bound_instance.get_grid_cell_dim().as_tuple() + def get_bound_min(self): + return self.bound_instance.get_bound_min() + def get_bound_max(self): + return self.bound_instance.get_bound_max() + + ## Structure management + + ## Options + + # Color + def set_color(self, val): + self.bound_instance.set_color(glm3(val)) + def get_color(self): + return self.bound_instance.get_color().as_tuple() + + # Edge Color + def set_edge_color(self, val): + self.bound_instance.set_edge_color(glm3(val)) + def get_edge_color(self): + return self.bound_instance.get_edge_color().as_tuple() + + # Edge width + def set_edge_width(self, val): + self.bound_instance.set_edge_width(val) + def get_edge_width(self): + return self.bound_instance.get_edge_width() + + # Edge width + def set_cube_size_factor(self, val): + self.bound_instance.set_cube_size_factor(val) + def get_cube_size_factor(self): + return self.bound_instance.get_cube_size_factor() + + # Material + def set_material(self, mat): + self.bound_instance.set_material(mat) + def get_material(self): + return self.bound_instance.get_material() + + ## Other stateful options + + def mark_nodes_as_used(self): + return self.bound_instance.mark_nodes_as_used() + def mark_cells_as_used(self): + return self.bound_instance.mark_cells_as_used() + + + ## Quantities + + # Scalar + + def add_scalar_quantity(self, name, values, defined_on='nodes', datatype="standard", **scalar_args): + + # NOTE: notice the .flatten('F') below to flatten in Fortran order. This assumes the input data is indexed like array[xInd,yInd,zInd], + # and converts to the internal x-changes-fastest data layout that the volume grid uses. + + if defined_on == 'nodes': + + if values.shape != self.get_grid_node_dim(): raise ValueError(f"'values' should be a {self.get_grid_node_dim()} array") + + q = self.bound_instance.add_node_scalar_quantity(name, values.flatten('F'), str_to_datatype(datatype)) + + elif defined_on == 'cells': + + if values.shape != self.get_grid_cell_dim(): raise ValueError(f"'values' should be a {self.get_grid_cell_dim()} array") + + q = self.bound_instance.add_cell_scalar_quantity(name, values.flatten('F'), str_to_datatype(datatype)) + + else: + raise ValueError("bad `defined_on` value {}, should be one of ['nodes', 'cells']".format(defined_on)) + + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_volume_grid_scalar_args(self, q, scalar_args, defined_on) + process_quantity_args(self, q, scalar_args) + process_scalar_args(self, q, scalar_args) + check_all_args_processed(self, q, scalar_args) + + + def add_scalar_quantity_from_callable(self, name, func, defined_on='nodes', datatype="standard", **scalar_args): + + if defined_on == 'nodes': + + q = self.bound_instance.add_node_scalar_quantity_from_callable(name, func, str_to_datatype(datatype)) + + elif defined_on == 'cells': + + q = self.bound_instance.add_cell_scalar_quantity_from_callable(name, func, str_to_datatype(datatype)) + + else: + raise ValueError("bad `defined_on` value {}, should be one of ['nodes', 'cells']".format(defined_on)) + + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_volume_grid_scalar_args(self, q, scalar_args, defined_on) + process_quantity_args(self, q, scalar_args) + process_scalar_args(self, q, scalar_args) + check_all_args_processed(self, q, scalar_args) + + +def register_volume_grid(name, bound_low, bound_high, node_dims, enabled=None, color=None, edge_color=None, edge_width=None, cube_size_factor=None, material=None, transparency=None): + + """Register a new volume grid""" + + if not psb.is_initialized(): + raise RuntimeError("Polyscope has not been initialized") + + p = VolumeGrid(name, node_dims=node_dims, bound_low=bound_low, bound_high=bound_high) + + # == Apply options + if enabled is not None: + p.set_enabled(enabled) + if color is not None: + p.set_color(color) + if edge_color is not None: + p.set_edge_color(edge_color) + if edge_width is not None: + p.set_edge_width(edge_width) + if cube_size_factor is not None: + p.set_cube_size_factor(cube_size_factor) + if material is not None: + p.set_material(material) + if transparency is not None: + p.set_transparency(transparency) + + return p + +def remove_volume_grid(name, error_if_absent=True): + """Remove a volume grid by name""" + psb.remove_volume_grid(name, error_if_absent) + +def get_volume_grid(name): + """Get volume grid by name""" + if not has_volume_grid(name): + raise ValueError("no volume grid with name " + str(name)) + + raw = psb.get_volume_grid(name) + + # Wrap the instance + return VolumeGrid(instance=raw) + +def has_volume_grid(name): + """Check if a volume grid exists by name""" + return psb.has_volume_grid(name) + diff --git a/src/polyscope/volume_mesh.py b/src/polyscope/volume_mesh.py index 8f994b6..f9eb284 100644 --- a/src/polyscope/volume_mesh.py +++ b/src/polyscope/volume_mesh.py @@ -2,17 +2,21 @@ import numpy as np from polyscope.core import str_to_datatype, str_to_vectortype, str_to_param_coords_type, str_to_param_viz_style, glm3 +from polyscope.structure import Structure +from polyscope.common import process_quantity_args, process_scalar_args, process_color_args, process_vector_args, check_all_args_processed -class VolumeMesh: +class VolumeMesh(Structure): # This class wraps a _reference_ to the underlying object, whose lifetime is managed by Polyscope # End users should not call this constrctor, use register_volume_mesh instead def __init__(self, name=None, vertices=None, tets=None, hexes=None, mixed_cells=None, instance=None): + + super().__init__() if instance is not None: # Wrap an existing instance - self.bound_mesh = instance + self.bound_instance = instance else: # Create a new instance @@ -26,15 +30,15 @@ def __init__(self, name=None, vertices=None, tets=None, hexes=None, mixed_cells= raise ValueError("specify EITHER mixed_cells OR tets/hexes but not both") if mixed_cells is not None: - self.bound_mesh = psb.register_volume_mesh(name, vertices, mixed_cells) + self.bound_instance = psb.register_volume_mesh(name, vertices, mixed_cells) else: if tets is None: - self.bound_mesh = psb.register_hex_mesh(name, vertices, hexes) + self.bound_instance = psb.register_hex_mesh(name, vertices, hexes) elif hexes is None: - self.bound_mesh = psb.register_tet_mesh(name, vertices, tets) + self.bound_instance = psb.register_tet_mesh(name, vertices, tets) else: - self.bound_mesh = psb.register_tet_hex_mesh(name, vertices, tets, hexes) + self.bound_instance = psb.register_tet_hex_mesh(name, vertices, tets, hexes) def check_shape(self, points): @@ -63,186 +67,127 @@ def check_index_array(self, arr, dim, name): def n_vertices(self): - return self.bound_mesh.n_vertices() + return self.bound_instance.n_vertices() def n_faces(self): - return self.bound_mesh.n_faces() + return self.bound_instance.n_faces() def n_cells(self): - return self.bound_mesh.n_cells() + return self.bound_instance.n_cells() ## Structure management - - def remove(self): - '''Remove the structure itself''' - self.bound_mesh.remove() - def remove_all_quantities(self): - '''Remove all quantities on the structure''' - self.bound_mesh.remove_all_quantities() - def remove_quantity(self, name): - '''Remove a single quantity on the structure''' - self.bound_mesh.remove_quantity(name) - - # Enable/disable - def set_enabled(self, val=True): - self.bound_mesh.set_enabled(val) - def is_enabled(self): - return self.bound_mesh.is_enabled() - - # Transparency - def set_transparency(self, val): - self.bound_mesh.set_transparency(val) - def get_transparency(self): - return self.bound_mesh.get_transparency() - - # Transformation things - def center_bounding_box(self): - self.bound_mesh.center_bounding_box() - def rescale_to_unit(self): - self.bound_mesh.rescale_to_unit() - def reset_transform(self): - self.bound_mesh.reset_transform() - def set_transform(self, new_mat4x4): - self.bound_mesh.set_transform(new_mat4x4) - def set_position(self, new_vec3): - self.bound_mesh.set_position(new_vec3) - def translate(self, trans_vec3): - self.bound_mesh.translate(trans_vec3) - def get_transform(self): - return self.bound_mesh.get_transform() - def get_position(self): - return self.bound_mesh.get_position() - - # Slice planes - def set_cull_whole_elements(self, val): - self.bound_mesh.set_cull_whole_elements(val) - def get_cull_whole_elements(self): - return self.bound_mesh.get_cull_whole_elements() - def set_ignore_slice_plane(self, plane, val): - # take either a string or a slice plane object as input - if isinstance(plane, str): - self.bound_mesh.set_ignore_slice_plane(plane, val) - else: - self.bound_mesh.set_ignore_slice_plane(plane.get_name(), val) - def get_ignore_slice_plane(self, plane): - # take either a string or a slice plane object as input - if isinstance(plane, str): - return self.bound_mesh.get_ignore_slice_plane(plane) - else: - return self.bound_mesh.get_ignore_slice_plane(plane.get_name()) - # Update def update_vertex_positions(self, vertices): self.check_shape(vertices) - self.bound_mesh.update_vertex_positions(vertices) + self.bound_instance.update_vertex_positions(vertices) ## Options # Color def set_color(self, val): - self.bound_mesh.set_color(glm3(val)) + self.bound_instance.set_color(glm3(val)) def get_color(self): - return self.bound_mesh.get_color().as_tuple() + return self.bound_instance.get_color().as_tuple() # Interior Color def set_interior_color(self, val): - self.bound_mesh.set_interior_color(glm3(val)) + self.bound_instance.set_interior_color(glm3(val)) def get_interior_color(self): - return self.bound_mesh.get_interior_color().as_tuple() + return self.bound_instance.get_interior_color().as_tuple() # Edge Color def set_edge_color(self, val): - self.bound_mesh.set_edge_color(glm3(val)) + self.bound_instance.set_edge_color(glm3(val)) def get_edge_color(self): - return self.bound_mesh.get_edge_color().as_tuple() + return self.bound_instance.get_edge_color().as_tuple() # Edge width def set_edge_width(self, val): - self.bound_mesh.set_edge_width(val) + self.bound_instance.set_edge_width(val) def get_edge_width(self): - return self.bound_mesh.get_edge_width() + return self.bound_instance.get_edge_width() # Material def set_material(self, mat): - self.bound_mesh.set_material(mat) + self.bound_instance.set_material(mat) def get_material(self): - return self.bound_mesh.get_material() + return self.bound_instance.get_material() ## Quantities # Scalar - def add_scalar_quantity(self, name, values, defined_on='vertices', enabled=None, datatype="standard", vminmax=None, cmap=None): + def add_scalar_quantity(self, name, values, defined_on='vertices', datatype="standard", **scalar_args): if len(values.shape) != 1: raise ValueError("'values' should be a length-N array") if defined_on == 'vertices': if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") - q = self.bound_mesh.add_vertex_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_vertex_scalar_quantity(name, values, str_to_datatype(datatype)) elif defined_on == 'cells': if values.shape[0] != self.n_cells(): raise ValueError("'values' should be a length n_cells array") - q = self.bound_mesh.add_cell_scalar_quantity(name, values, str_to_datatype(datatype)) + q = self.bound_instance.add_cell_scalar_quantity(name, values, str_to_datatype(datatype)) else: raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'cells']".format(defined_on)) - - - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if vminmax is not None: - q.set_map_range(vminmax) - if cmap is not None: - q.set_color_map(cmap) + + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, scalar_args) + process_scalar_args(self, q, scalar_args) + check_all_args_processed(self, q, scalar_args) + # Color - def add_color_quantity(self, name, values, defined_on='vertices', enabled=None): + def add_color_quantity(self, name, values, defined_on='vertices', **color_args): if len(values.shape) != 2 or values.shape[1] != 3: raise ValueError("'values' should be an Nx3 array") if defined_on == 'vertices': if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") - q = self.bound_mesh.add_vertex_color_quantity(name, values) + q = self.bound_instance.add_vertex_color_quantity(name, values) elif defined_on == 'cells': if values.shape[0] != self.n_cells(): raise ValueError("'values' should be a length n_cells array") - q = self.bound_mesh.add_cell_color_quantity(name, values) + q = self.bound_instance.add_cell_color_quantity(name, values) else: raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'cells']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, color_args) + process_color_args(self, q, color_args) + check_all_args_processed(self, q, color_args) # Vector - def add_vector_quantity(self, name, values, defined_on='vertices', enabled=None, vectortype="standard", length=None, radius=None, color=None): + def add_vector_quantity(self, name, values, defined_on='vertices', vectortype="standard", **vector_args): if len(values.shape) != 2 or values.shape[1] != 3: raise ValueError("'values' should be an Nx3 array") if defined_on == 'vertices': if values.shape[0] != self.n_vertices(): raise ValueError("'values' should be a length n_vertices array") - q = self.bound_mesh.add_vertex_vector_quantity(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_vertex_vector_quantity(name, values, str_to_vectortype(vectortype)) elif defined_on == 'cells': if values.shape[0] != self.n_cells(): raise ValueError("'values' should be a length n_cells array") - q = self.bound_mesh.add_cell_vector_quantity(name, values, str_to_vectortype(vectortype)) + q = self.bound_instance.add_cell_vector_quantity(name, values, str_to_vectortype(vectortype)) else: raise ValueError("bad `defined_on` value {}, should be one of ['vertices', 'cells']".format(defined_on)) - # Support optional params - if enabled is not None: - q.set_enabled(enabled) - if length is not None: - q.set_length(length, True) - if radius is not None: - q.set_radius(radius, True) - if color is not None: - q.set_color(glm3(color)) + + # process and act on additional arguments + # note: each step modifies the args dict and removes processed args + process_quantity_args(self, q, vector_args) + process_vector_args(self, q, vector_args) + check_all_args_processed(self, q, vector_args) def register_volume_mesh(name, vertices, tets=None, hexes=None, mixed_cells=None, enabled=None, color=None, interior_color=None, edge_color=None, edge_width=None, material=None, transparency=None): - """Register a new surface mesh""" - if not psb.isInitialized(): + """Register a new volume mesh""" + + if not psb.is_initialized(): raise RuntimeError("Polyscope has not been initialized") p = VolumeMesh(name, vertices=vertices, tets=tets, hexes=hexes, mixed_cells=mixed_cells) diff --git a/src/polyscope_bindings/imgui.pyi b/src/polyscope_bindings/imgui.pyi index d4bf983..e582eed 100644 --- a/src/polyscope_bindings/imgui.pyi +++ b/src/polyscope_bindings/imgui.pyi @@ -27,6 +27,10 @@ ImGuiTabItemFlags = NewType("ImGuiTabItemFlags", int) ImGuiTreeNodeFlags = NewType("ImGuiTreeNodeFlags", int) ImGuiWindowFlags = NewType("ImGuiWindowFlags", int) +# Draw Types +ImDrawFlags = NewType("ImDrawFlags", int) +ImDrawListFlags = NewType("ImDrawListFlags", int) + # Windows @@ -887,6 +891,27 @@ def SaveIniSettingsToDisk(ini_filename: str) -> None: ... def SaveIniSettingsToMemory() -> str: ... +# Draw Commands + +def AddLine(p1: ImVec2, p2: ImVec2, col: ImU32, thickness: float = 1.) -> None: ... +def AddRect(p_min: ImVec2, p_max: ImVec2, col: ImU32, rounding: float = 0., flags: ImDrawFlags = 0, thickness: float = 1.) -> None: ... +def AddRectFilled(p_min: ImVec2, p_max: ImVec2, col: ImU32, rounding: float = 0., flags: ImDrawFlags = 0) -> None: ... +def AddAddRectFilledMultiColor(p_min: ImVec2, p_max: ImVec2, col_upr_left: ImU32, col_upr_right: ImU32, col_bot_right: ImU32, col_bot_left: ImU32) -> None: ... +def AddQuad(p1: ImVec2, p2: ImVec2, p3: ImVec2, p4: ImVec2, col: ImU32, thickness: float = 1.) -> None: ... +def AddQuadFilled(p1: ImVec2, p2: ImVec2, p3: ImVec2, p4: ImVec2, col: ImU32) -> None: ... +def AddTriangle(p1: ImVec2, p2: ImVec2, p3: ImVec2, col: ImU32, thickness: float = 1.) -> None: ... +def AddTriangleFilled(p1: ImVec2, p2: ImVec2, p3: ImVec2, col: ImU32) -> None: ... +def AddCircle(center: ImVec2, radius: float, col: ImU32, num_segments: int = 0, thickness: float = 1.) -> None: ... +def AddCircleFilled(center: ImVec2, radius: float, col: ImU32, num_segments: int = 0) -> None: ... +def AddNgon(center: ImVec2, radius: float, col: ImU32, num_segments: int, thickness: float = 1.) -> None: ... +def AddNgonFilled(center: ImVec2, radius: float, col: ImU32, num_segments: int) -> None: ... +def AddText(pos: ImVec2, col: ImU32, text: str, text_end: Optional[str] = None) -> None: ... +def AddPolyline(points: List[ImVec2], num_points: int, col: ImU32, flags: ImDrawFlags, thickness) -> None: ... +def AddConvexPolyFilled(points: List[ImVec2], num_points: int, col: ImU32) -> None: ... +def AddBezierCubic(p1: ImVec2, p2: ImVec2, p3: ImVec2, p4: ImVec2, col: ImU32, thickness: float, num_segments: int = 0) -> None: ... +def AddBezierQuadratic(p1: ImVec2, p2: ImVec2, p3: ImVec2, col: ImU32, thickness: float, num_segments: int = 0) -> None: ... + + ImGuiWindowFlags_None: int ImGuiWindowFlags_NoTitleBar: int ImGuiWindowFlags_NoResize: int diff --git a/test/fast_update_demo.py b/test/fast_update_demo.py new file mode 100644 index 0000000..6f9ebb3 --- /dev/null +++ b/test/fast_update_demo.py @@ -0,0 +1,112 @@ +import os +import sys +import os.path as path + +# Path to where the bindings live +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) +if os.name == 'nt': # if Windows + # handle default location where VS puts binary + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Debug"))) +else: + # normal / unix case + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build"))) + +import polyscope as ps +import polyscope.imgui as psim +from polyscope import imgui as psim +import potpourri3d as pp3d + +import sys +import argparse +import numpy as np + +import torch as torch + +def toNP(arr): + return arr.detach().cpu().numpy() + +def main(): + + parser = argparse.ArgumentParser() + + # Build arguments + + # Parse arguments + args = parser.parse_args() + + device = torch.device('cuda:0') + #device = torch.device('cpu') + dtype = torch.float32 + + ui_nPts = 10000 + ui_gpu_update = True + ui_step_size = 0.01 + + nPts = ui_nPts + step_size = ui_step_size + gpu_update = ui_gpu_update + + pos = torch.zeros((nPts,3), device=device, dtype=dtype) + ps_pts = None + + def reinitialize(): + nonlocal nPts, ui_nPts, step_size, ui_step_size, gpu_update, ui_gpu_update, pos, ps_pts + + nPts = ui_nPts + step_size = ui_step_size + gpu_update = ui_gpu_update + + pos = torch.zeros((nPts,3), device=device, dtype=dtype) + + ps_pts = ps.register_point_cloud("points", toNP(pos), point_render_mode='quad') + + def step_simulation(): + nonlocal pos + + steps = torch.randn((nPts, 3), dtype=pos.dtype, device=pos.device) * step_size + pos += steps + + def update_viz(): + nonlocal pos + + if gpu_update: + ps_pts.get_buffer('points').update_data_from_device(pos) + + else: + ps_pts.update_point_positions(toNP(pos)) + + def callback(): + nonlocal ui_nPts, ui_step_size, ui_gpu_update + + _, ui_nPts = psim.InputInt("nPts", ui_nPts) + _, ui_step_size = psim.InputFloat("step size", ui_step_size) + _, ui_gpu_update = psim.Checkbox("gpu update", ui_gpu_update) + + # Executed every frame + if(psim.Button("Re-initialize")): + reinitialize() + + step_simulation() + update_viz() + + + ps.set_user_callback(callback) + + ps.set_automatically_compute_scene_extents(False) + ps.set_length_scale(1.) + low = np.array((-1, -1., -1.)) + high = np.array((1., 1., 1.)) + ps.set_bounding_box(low, high) + + # Always initialize exactly once + ps.init() + + reinitialize() + + ps.set_ground_plane_mode("shadow_only") + + ps.show() + + +if __name__ == '__main__': + main() diff --git a/test/imgui_demo.py b/test/imgui_demo.py index 3349ceb..7861938 100644 --- a/test/imgui_demo.py +++ b/test/imgui_demo.py @@ -31,6 +31,8 @@ def main(): ui_text = "some input text" ui_options = ["option A", "option B", "option C"] ui_options_selected = ui_options[1] + + alt_font = None def my_function(): # ... do something important here ... @@ -149,8 +151,27 @@ def callback(): psim.PopItemWidth() + # Create an annotation window, use a custom font + if False: + psim.SetNextWindowPos((200, 200)) + psim.SetNextWindowSize((100, 50)) + psim.SetNextWindowBgAlpha(0.8) + psim.Begin("Annotation Window", None, psim.ImGuiWindowFlags_NoDecoration) + psim.PushFont(alt_font) + psim.TextUnformatted("annotation text") + psim.PopFont() + psim.End() + polyscope.init() polyscope.set_user_callback(callback) + + + # Load a custom font + if False: + io = psim.GetIO() + alt_font = io.Fonts.AddFontFromFileTTF("your_font_file.otf", 50) + + polyscope.show() if __name__ == '__main__': diff --git a/test/polyscope_demo.py b/test/polyscope_demo.py index 33dab5f..d4503f2 100644 --- a/test/polyscope_demo.py +++ b/test/polyscope_demo.py @@ -15,28 +15,51 @@ import polyscope.imgui as psim from polyscope import imgui as psim import potpourri3d as pp3d +import igl +import imageio import sys import argparse import numpy as np +# Helper funcs for defining implicits +def sphere_sdf(pts): + res = np.linalg.norm(pts, axis=-1) - 1. + return res +def color_func(pts): + # return np.cos(3*pts)**2 + A = np.ones_like(pts) * 0.3 + A[:,0] = np.cos(3*pts[:,0])**2 + return A + + def main(): parser = argparse.ArgumentParser() # Build arguments - parser.add_argument('mesh', type=str, help='path to a mesh') + # parser.add_argument('mesh', type=str, help='path to a mesh') + # parser.add_argument('image', type=str, help='path to a image') parser.add_argument('--surfacemesh', default=True, action='store_true') parser.add_argument('--no-surfacemesh', dest='surfacemesh', action='store_false') parser.add_argument('--pointcloud', default=False, action='store_true') parser.add_argument('--volumemesh', default=False, action='store_true') + parser.add_argument('--volumegrid', default=False, action='store_true') # Parse arguments args = parser.parse_args() # Load a mesh argument - verts, faces = pp3d.read_mesh(args.mesh) - + # verts, faces = pp3d.read_mesh(args.mesh) + + # verts, UVs, _, faces, UVinds, _ = igl.read_obj(args.mesh) + # corner_UVs = UVs[UVinds,:].reshape(-1,2) + # print(corner_UVs.shape) + # + # color_tex = imageio.imread(args.image) / 255. + # print(color_tex.shape) + + # print(UVs[:10,:]) # Set up a simple callback and UI button def my_function(): @@ -44,8 +67,12 @@ def my_function(): def callback(): # Executed every frame + if(psim.Button("Test button")): my_function() + + implicit_ui() + polyscope.set_user_callback(callback) @@ -55,6 +82,7 @@ def callback(): polyscope.init() polyscope.set_ground_plane_mode("shadow_only") + polyscope.set_verbosity(101) ## Examples with a mesh if args.surfacemesh: @@ -64,6 +92,13 @@ def callback(): ps_mesh.add_scalar_quantity("X", verts[:,0]) ps_mesh.add_scalar_quantity("Y", verts[:,1]) + ps_mesh.add_parameterization_quantity("param", corner_UVs, defined_on='corners') + + ps_mesh.add_color_quantity("color_tex", color_tex, defined_on='texture', param_name="param", image_origin='upper_left') + ps_mesh.add_scalar_quantity("scalar_tex", color_tex[:,:,1], defined_on='texture', param_name="param") + + polyscope.add_color_image_quantity("my im", color_tex) + # Look at them polyscope.show() @@ -189,11 +224,41 @@ def callback(): # Remove the whole mesh structure polyscope.remove_all_structures() + + + if args.volumegrid: + +# Any implicit function mapping [N,3] numpy array +# of locations --> [N] numpy array of values +def sphere_sdf(pts): + res = np.linalg.norm(pts, axis=-1) - 1. + return res + +# Create the grid structure +bound_min = np.array([-1., -1., -1.]) +bound_max = np.array([+1., +1., +1.]) +node_dims = np.array([128,128,128]) +ps_grid = polyscope.register_volume_grid("test volume grid", bound_min, bound_max, node_dims) + +# This makes polyscope register the scalar quantity by calling +# your function for each point on the grid (so you don't have +# to worry about getting indexing right) +ps_grid.add_scalar_quantity_from_callable("sdf", sphere_sdf) + +polyscope.show() # Back to empty polyscope.show() # polyscope.clear_user_callback() +def implicit_ui(): + + if(psim.Button("Render sphere SDF")): + # polyscope.render_implicit_surface("sphere sdf", sphere_sdf, 'sphere_march', enabled=True) + polyscope.render_implicit_surface_color("sphere sdf", sphere_sdf, color_func, 'sphere_march', enabled=True) + + + if __name__ == '__main__': main() diff --git a/test/polyscope_test.py b/test/polyscope_test.py index 1d43299..a80dd3d 100644 --- a/test/polyscope_test.py +++ b/test/polyscope_test.py @@ -22,12 +22,40 @@ # Path to test assets assets_prefix = path.join(path.dirname(__file__), "assets/") +def assertArrayWithShape(self, arr, shape): + self.assertTrue(isinstance(arr, np.ndarray)) + self.assertEqual(tuple(arr.shape), tuple(shape)) + class TestCore(unittest.TestCase): def test_show(self): ps.show(forFrames=3) + def test_frame_tick(self): + for i in range(4): + ps.frame_tick() + + def test_frame_tick_imgui(self): + def callback(): + psim.Button("test button") + ps.set_user_callback(callback) + for i in range(4): + ps.frame_tick() + + def test_unshow(self): + counts = [0] + def callback(): + counts[0] = counts[0] + 1 + if(counts[0] > 2): + ps.unshow() + + ps.set_user_callback(callback) + ps.show(10) + + self.assertLess(counts[0], 4) + + def test_options(self): # Remember, polyscope has global state, so we're actually setting these for the remainder of the tests (lol) @@ -37,6 +65,7 @@ def test_options(self): ps.set_print_prefix("polyscope test") ps.set_errors_throw_exceptions(True) ps.set_max_fps(60) + ps.set_enable_vsync(True) ps.set_use_prefs_file(True) ps.set_always_redraw(False) @@ -45,11 +74,22 @@ def test_options(self): ps.set_autocenter_structures(False) ps.set_autoscale_structures(False) + ps.request_redraw() + self.assertTrue(ps.get_redraw_requested()) + ps.set_always_redraw(False) + ps.set_build_gui(True) + ps.set_render_scene(True) ps.set_open_imgui_window_for_user_callback(True) ps.set_invoke_user_callback_for_nested_show(False) ps.set_give_focus_on_show(True) + ps.set_background_color((0.7, 0.8, 0.9)) + ps.set_background_color((0.7, 0.8, 0.9, 0.9)) + ps.get_background_color() + + ps.get_final_scene_color_texture_native_handle() + ps.show(3) def test_callbacks(self): @@ -80,6 +120,9 @@ def test_view_options(self): ps.set_navigation_style("turntable") ps.set_navigation_style("free") ps.set_navigation_style("planar") + ps.set_navigation_style("none") + ps.set_navigation_style("first_person") + ps.set_navigation_style(ps.get_navigation_style()) ps.set_up_dir("x_up") ps.set_up_dir("neg_x_up") @@ -87,10 +130,30 @@ def test_view_options(self): ps.set_up_dir("neg_y_up") ps.set_up_dir("z_up") ps.set_up_dir("neg_z_up") + ps.set_up_dir(ps.get_up_dir()) + + ps.set_front_dir("x_front") + ps.set_front_dir("neg_x_front") + ps.set_front_dir("y_front") + ps.set_front_dir("neg_y_front") + ps.set_front_dir("z_front") + ps.set_front_dir("neg_z_front") + ps.set_front_dir(ps.get_front_dir()) ps.set_view_projection_mode("orthographic") ps.set_view_projection_mode("perspective") + ps.set_camera_view_matrix(ps.get_camera_view_matrix()) + + ps.set_window_size(800, 600) + self.assertEqual(ps.get_window_size(), (800,600)) + + tup = ps.get_buffer_size() + w, h = int(tup[0]), int(tup[1]) + + ps.set_window_resizable(True) + self.assertEqual(ps.get_window_resizable(), True) + ps.show(3) ps.set_up_dir("y_up") @@ -107,6 +170,9 @@ def test_camera_movement(self): ps.show(3) + def test_view_json(self): + ps.set_view_from_json(ps.get_view_as_json()) + def test_ground_options(self): ps.set_ground_plane_mode("none") @@ -208,6 +274,78 @@ def test_scene_extents(self): ps.set_automatically_compute_scene_extents(True) + def test_groups(self): + + pts = np.zeros((10,3)) + pt_cloud_0 = ps.register_point_cloud("cloud0", pts) + pt_cloud_1 = ps.register_point_cloud("cloud1", pts) + pt_cloud_2 = ps.register_point_cloud("cloud2", pts) + + groupA = ps.create_group("group_A") + groupB = ps.create_group("group_B") + groupC = ps.create_group("group_C") + + groupA.add_child_group(groupB) + groupA.add_child_group("group_C") + + pt_cloud_0.add_to_group(groupA) + pt_cloud_1.add_to_group("group_A") + groupA.add_child_structure(pt_cloud_2) + + groupA.set_enabled(False) + groupB.set_show_child_details(True) + groupC.set_hide_descendants_from_structure_lists(True) + + ps.show(3) + + groupA.remove_child_group(groupB) + groupA.remove_child_group("group_C") + groupA.remove_child_structure(pt_cloud_0) + + ps.remove_group(groupB, True) + ps.remove_group("group_C", False) + + ps.show(3) + + ps.remove_all_groups() + + ps.remove_all_structures() + + + def test_groups_demo_example(self): + + # make a point cloud + pts = np.zeros((300,3)) + psCloud = ps.register_point_cloud("my cloud", pts) + + # make a curve network + nodes = np.zeros((4,3)) + edges = np.array([[1, 3], [3, 0], [1, 0], [0, 2]]) + psCurve = ps.register_curve_network("my network", nodes, edges) + + # create a group for these two objects + group = ps.create_group("my group") + psCurve.add_to_group(group) # you also say psCurve.add_to_group("my group") + psCloud.add_to_group(group) + + # toggle the enabled state for everything in the group + group.set_enabled(False) + + # hide items in group from displaying in the UI + # (useful if you are registering huge numbers of structures you don't always need to see) + group.set_hide_descendants_from_structure_lists(True) + group.set_show_child_details(False) + + # nest groups inside of other groups + super_group = ps.create_group("py parent group") + super_group.add_child_group(group) + + ps.show(3) + + ps.remove_all_groups() + ps.remove_all_structures() + + class TestImGuiBindings(unittest.TestCase): def test_ui_calls(self): @@ -954,7 +1092,16 @@ def test_options(self): # Transparency p.set_transparency(0.8) self.assertAlmostEqual(0.8, p.get_transparency()) - + + # Mark elements as used + # p.set_corner_permutation(np.random.permutation(p.n_corners())) # not required + p.mark_corners_as_used() + p.set_edge_permutation(np.random.permutation(p.n_edges())) + p.mark_edges_as_used() + p.set_halfedge_permutation(np.random.permutation(p.n_halfedges())) + p.mark_halfedges_as_used() + + # Set with optional arguments p2 = ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces(), enabled=True, material='wax', color=(1., 0., 0.), edge_color=(0.5, 0.5, 0.5), @@ -1017,28 +1164,18 @@ def test_2D(self): def test_permutation(self): - p = ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) - p.set_vertex_permutation(np.random.permutation(p.n_vertices())) - p.set_vertex_permutation(np.random.permutation(p.n_vertices()), 3*p.n_vertices()) + p = ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) - p.set_face_permutation(np.random.permutation(p.n_faces())) - p.set_face_permutation(np.random.permutation(p.n_faces()), 3*p.n_faces()) - p.set_edge_permutation(np.random.permutation(p.n_edges())) - p.set_edge_permutation(np.random.permutation(p.n_edges()), 3*p.n_edges()) p.set_corner_permutation(np.random.permutation(p.n_corners())) - p.set_corner_permutation(np.random.permutation(p.n_corners()), 3*p.n_corners()) p.set_halfedge_permutation(np.random.permutation(p.n_halfedges())) - p.set_halfedge_permutation(np.random.permutation(p.n_halfedges()), 3*p.n_halfedges()) p = ps.register_surface_mesh("test_mesh2", self.generate_verts(), self.generate_faces()) p.set_all_permutations( - vertex_perm=np.random.permutation(p.n_vertices()), - face_perm=np.random.permutation(p.n_faces()), edge_perm=np.random.permutation(p.n_edges()), corner_perm=np.random.permutation(p.n_corners()), halfedge_perm=np.random.permutation(p.n_halfedges()), @@ -1046,40 +1183,62 @@ def test_permutation(self): ps.show(3) ps.remove_all_structures() + + def test_permutation_with_size(self): - - def test_tangent_basis(self): p = ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) - p.set_vertex_tangent_basisX(np.random.rand(p.n_vertices(), 3)) - p.set_vertex_tangent_basisX(np.random.rand(p.n_vertices(), 2)) + p.set_edge_permutation(np.random.permutation(p.n_edges()), 3*p.n_edges()) + + p.set_corner_permutation(np.random.permutation(p.n_corners()), 3*p.n_corners()) + + p.set_halfedge_permutation(np.random.permutation(p.n_halfedges()), 3*p.n_halfedges()) - p.set_face_tangent_basisX(np.random.rand(p.n_faces(), 3)) - p.set_face_tangent_basisX(np.random.rand(p.n_faces(), 2)) + p = ps.register_surface_mesh("test_mesh2", self.generate_verts(), self.generate_faces()) + + p.set_all_permutations( + edge_perm=np.random.permutation(p.n_edges()), + edge_perm_size=3*p.n_edges(), + corner_perm=np.random.permutation(p.n_corners()), + corner_perm_size=3*p.n_corners(), + halfedge_perm=np.random.permutation(p.n_halfedges()), + halfedge_perm_size=p.n_halfedges(), + ) ps.show(3) ps.remove_all_structures() + def test_scalar(self): - ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) - p = ps.get_surface_mesh("test_mesh") - for on in ['vertices', 'faces', 'edges', 'halfedges']: - + for on in ['vertices', 'faces', 'edges', 'halfedges', 'texture']: + + ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) + p = ps.get_surface_mesh("test_mesh") + + param_name = None # used for texture case only + if on == 'vertices': vals = np.random.rand(p.n_vertices()) elif on == 'faces': vals = np.random.rand(p.n_faces()) elif on == 'edges': vals = np.random.rand(p.n_edges()) + p.set_edge_permutation(np.random.permutation(p.n_edges())) elif on == 'halfedges': vals = np.random.rand(p.n_halfedges()) + elif on == 'texture': + param_vals = np.random.rand(p.n_vertices(), 2) + param_name = "test_param" + p.add_parameterization_quantity(param_name, param_vals, defined_on='vertices', enabled=True) + vals = np.random.rand(20,30) - p.add_scalar_quantity("test_vals", vals, defined_on=on) - p.add_scalar_quantity("test_vals2", vals, defined_on=on, enabled=True) - p.add_scalar_quantity("test_vals_with_range", vals, defined_on=on, vminmax=(-5., 5.), enabled=True) - p.add_scalar_quantity("test_vals_with_datatype", vals, defined_on=on, enabled=True, datatype='symmetric') - p.add_scalar_quantity("test_vals_with_cmap", vals, defined_on=on, enabled=True, cmap='blues') + + p.add_scalar_quantity("test_vals", vals, defined_on=on, param_name=param_name) + p.add_scalar_quantity("test_vals2", vals, defined_on=on, param_name=param_name, enabled=True) + p.add_scalar_quantity("test_vals_with_range", vals, defined_on=on, param_name=param_name, vminmax=(-5., 5.), enabled=True) + p.add_scalar_quantity("test_vals_with_datatype", vals, defined_on=on, param_name=param_name, enabled=True, datatype='symmetric') + p.add_scalar_quantity("test_vals_with_cmap", vals, defined_on=on, param_name=param_name, enabled=True, cmap='blues') ps.show(3) @@ -1089,22 +1248,29 @@ def test_scalar(self): p.remove_all_quantities() p.remove_all_quantities() - ps.remove_all_structures() + ps.remove_all_structures() def test_color(self): ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) p = ps.get_surface_mesh("test_mesh") - for on in ['vertices', 'faces']: - + for on in ['vertices', 'faces', 'texture']: + + param_name = None # used for texture case only + if on == 'vertices': vals = np.random.rand(p.n_vertices(), 3) elif on == 'faces': vals = np.random.rand(p.n_faces(), 3) + elif on == 'texture': + param_vals = np.random.rand(p.n_vertices(), 2) + param_name = "test_param" + p.add_parameterization_quantity(param_name, param_vals, defined_on='vertices', enabled=True) + vals = np.random.rand(20,30,3) - p.add_color_quantity("test_vals", vals, defined_on=on) - p.add_color_quantity("test_vals", vals, defined_on=on, enabled=True) + p.add_color_quantity("test_vals", vals, defined_on=on, param_name=param_name) + p.add_color_quantity("test_vals", vals, defined_on=on, param_name=param_name, enabled=True) ps.show(3) p.remove_all_quantities() @@ -1200,7 +1366,7 @@ def test_vector(self): ps.remove_all_structures() - def test_intrinsic_vector(self): + def test_tangent_vector(self): ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) p = ps.get_surface_mesh("test_mesh") @@ -1208,31 +1374,34 @@ def test_intrinsic_vector(self): for on in ['vertices', 'faces']: if on == 'vertices': - vals = np.random.rand(p.n_vertices(),2) - p.set_vertex_tangent_basisX(np.random.rand(p.n_vertices(), 3)); + vals = np.random.rand(p.n_vertices(), 2) + basisX = np.random.rand(p.n_vertices(), 3) + basisY = np.random.rand(p.n_vertices(), 3) elif on == 'faces': vals = np.random.rand(p.n_faces(), 2) - p.set_face_tangent_basisX(np.random.rand(p.n_faces(), 3)); - - p.add_intrinsic_vector_quantity("test_vals1", vals, defined_on=on) - p.add_intrinsic_vector_quantity("test_vals2", vals, defined_on=on, enabled=True) - p.add_intrinsic_vector_quantity("test_vals3", vals, defined_on=on, enabled=True, vectortype='ambient') - p.add_intrinsic_vector_quantity("test_vals4", vals, defined_on=on, enabled=True, length=0.005) - p.add_intrinsic_vector_quantity("test_vals5", vals, defined_on=on, enabled=True, radius=0.001) - p.add_intrinsic_vector_quantity("test_vals6", vals, defined_on=on, enabled=True, color=(0.2, 0.5, 0.5)) - p.add_intrinsic_vector_quantity("test_vals7", vals, defined_on=on, enabled=True, radius=0.001, ribbon=True) - p.add_intrinsic_vector_quantity("test_vals8", vals, n_sym=4, defined_on=on, enabled=True) + basisX = np.random.rand(p.n_faces(), 3) + basisY = np.random.rand(p.n_faces(), 3) + + p.add_tangent_vector_quantity("test_vals1", vals, basisX, basisY, defined_on=on) + p.add_tangent_vector_quantity("test_vals2", vals, basisX, basisY, defined_on=on, enabled=True) + p.add_tangent_vector_quantity("test_vals3", vals, basisX, basisY, defined_on=on, enabled=True, vectortype='ambient') + p.add_tangent_vector_quantity("test_vals4", vals, basisX, basisY, defined_on=on, enabled=True, length=0.005) + p.add_tangent_vector_quantity("test_vals5", vals, basisX, basisY, defined_on=on, enabled=True, radius=0.001) + p.add_tangent_vector_quantity("test_vals6", vals, basisX, basisY, defined_on=on, enabled=True, color=(0.2, 0.5, 0.5)) + p.add_tangent_vector_quantity("test_vals7", vals, basisX, basisY, defined_on=on, enabled=True, radius=0.001) + p.add_tangent_vector_quantity("test_vals8", vals, basisX, basisY, n_sym=4, defined_on=on, enabled=True) ps.show(3) p.remove_all_quantities() ps.remove_all_structures() - def test_one_form_intrinsic_vector(self): + def test_one_form_tangent_vector(self): ps.register_surface_mesh("test_mesh", self.generate_verts(), self.generate_faces()) p = ps.get_surface_mesh("test_mesh") - p.set_vertex_tangent_basisX(np.random.rand(p.n_vertices(), 3)); + + p.set_edge_permutation(np.random.permutation(p.n_edges())) vals = np.random.rand(p.n_edges()) orients = np.random.rand(p.n_edges()) > 0.5 @@ -1243,7 +1412,7 @@ def test_one_form_intrinsic_vector(self): p.add_one_form_vector_quantity("test_vals4", vals, orients, enabled=True, length=0.005) p.add_one_form_vector_quantity("test_vals5", vals, orients, enabled=True, radius=0.001) p.add_one_form_vector_quantity("test_vals6", vals, orients, enabled=True, color=(0.2, 0.5, 0.5)) - p.add_one_form_vector_quantity("test_vals7", vals, orients, enabled=True, radius=0.001, ribbon=True) + p.add_one_form_vector_quantity("test_vals7", vals, orients, enabled=True, radius=0.001) ps.show(3) p.remove_all_quantities() @@ -1499,6 +1668,639 @@ def test_vector(self): ps.remove_all_structures() +class TestVolumeGrid(unittest.TestCase): + + def test_add_remove(self): + + # add + n = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + self.assertTrue(ps.has_volume_grid("test_grid")) + self.assertFalse(ps.has_volume_grid("nope")) + self.assertEqual(n.n_nodes(), 10*12*14) + self.assertEqual(n.n_cells(), (10-1)*(12-1)*(14-1)) + + # remove by name + ps.register_volume_grid("test_grid2", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + ps.remove_volume_grid("test_grid2") + self.assertTrue(ps.has_volume_grid("test_grid")) + self.assertFalse(ps.has_volume_grid("test_grid2")) + + # remove by ref + c = ps.register_volume_grid("test_grid2", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + c.remove() + self.assertTrue(ps.has_volume_grid("test_grid")) + self.assertFalse(ps.has_volume_grid("test_grid2")) + + # get by name + ps.register_volume_grid("test_grid3", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + p = ps.get_volume_grid("test_grid3") # should be wrapped instance, not underlying PSB instance + self.assertTrue(isinstance(p, ps.VolumeGrid)) + + ps.remove_all_structures() + + def test_render(self): + + ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + ps.show(3) + ps.remove_all_structures() + + def test_options(self): + + p = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + + # misc getters + p.n_nodes() + p.n_cells() + p.grid_spacing() + self.assertTrue((p.get_grid_node_dim() == (10,12,14))) + self.assertTrue((p.get_grid_cell_dim() == ((10-1),(12-1),(14-1)))) + self.assertTrue((p.get_bound_min() == np.array((0., 0., 0.))).all()) + self.assertTrue((p.get_bound_max() == np.array((1., 1., 1.))).all()) + + # Set enabled + p.set_enabled() + p.set_enabled(False) + p.set_enabled(True) + self.assertTrue(p.is_enabled()) + + # Color + color = (0.3, 0.3, 0.5) + p.set_color(color) + ret_color = p.get_color() + for i in range(3): + self.assertAlmostEqual(ret_color[i], color[i]) + + # Edge color + color = (0.1, 0.5, 0.5) + p.set_edge_color(color) + ret_color = p.get_edge_color() + for i in range(3): + self.assertAlmostEqual(ret_color[i], color[i]) + + ps.show(3) + + # Edge width + p.set_edge_width(1.5) + ps.show(3) + self.assertAlmostEqual(p.get_edge_width(), 1.5) + + # Cube size factor + p.set_cube_size_factor(0.5) + ps.show(3) + self.assertAlmostEqual(p.get_cube_size_factor(), 0.5) + + # Material + p.set_material("candy") + self.assertEqual("candy", p.get_material()) + p.set_material("clay") + + # Transparency + p.set_transparency(0.8) + self.assertAlmostEqual(0.8, p.get_transparency()) + + # Set with optional arguments + p2 = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), (10,12,14), + enabled=True, material='wax', color=(1., 0., 0.), edge_color=(0.5, 0.5, 0.5), edge_width=0.5, cube_size_factor=0.5, transparency=0.9) + + ps.show(3) + + ps.remove_all_structures() + ps.set_transparency_mode('none') + + def test_transform(self): + + p = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + test_transforms(self,p) + ps.remove_all_structures() + + def test_slice_plane(self): + + p = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), (10,12,14)) + + plane = ps.add_scene_slice_plane() + p.set_cull_whole_elements(True) + ps.show(3) + p.set_cull_whole_elements(False) + ps.show(3) + + p.set_ignore_slice_plane(plane, True) + self.assertEqual(True, p.get_ignore_slice_plane(plane)) + p.set_ignore_slice_plane(plane.get_name(), False) + self.assertEqual(False, p.get_ignore_slice_plane(plane.get_name())) + + ps.show(3) + + ps.remove_all_structures() + ps.remove_last_scene_slice_plane() + + + def test_scalar(self): + node_dim = (10,12,14) + cell_dim = (10-1,12-1,14-1) + p = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), node_dim) + + for on in ['nodes', 'cells']: + + if on == 'nodes': + vals = np.random.rand(*node_dim) + elif on == 'cells': + vals = np.random.rand(*cell_dim) + + p.add_scalar_quantity("test_vals", vals, defined_on=on) + p.add_scalar_quantity("test_vals2", vals, defined_on=on, enabled=True) + p.add_scalar_quantity("test_vals_with_range", vals, defined_on=on, vminmax=(-5., 5.), enabled=True) + p.add_scalar_quantity("test_vals_with_datatype", vals, defined_on=on, enabled=True, datatype='symmetric') + p.add_scalar_quantity("test_vals_with_cmap", vals, defined_on=on, enabled=True, cmap='blues', enable_gridcube_viz=False) + + ps.show(3) + + # test some additions/removal while we're at it + p.remove_quantity("test_vals") + p.remove_quantity("not_here") # should not error + p.remove_all_quantities() + p.remove_all_quantities() + + ps.remove_all_structures() + + def test_scalar_from_callable(self): + node_dim = (10,12,14) + cell_dim = (10-1,12-1,14-1) + p = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), node_dim) + + def sphere_sdf(pts): return np.linalg.norm(pts, axis=-1) - 1. + + for on in ['nodes', 'cells']: + + p.add_scalar_quantity_from_callable("test_vals", sphere_sdf, defined_on=on) + p.add_scalar_quantity_from_callable("test_vals2", sphere_sdf, defined_on=on, enabled=True) + p.add_scalar_quantity_from_callable("test_vals_with_range", sphere_sdf, defined_on=on, vminmax=(-5., 5.), enabled=True) + p.add_scalar_quantity_from_callable("test_vals_with_datatype", sphere_sdf, defined_on=on, enabled=True, datatype='symmetric') + p.add_scalar_quantity_from_callable("test_vals_with_cmap", sphere_sdf, defined_on=on, enabled=True, cmap='blues', enable_gridcube_viz=False) + + ps.show(3) + + # test some additions/removal while we're at it + p.remove_quantity("test_vals") + p.remove_quantity("not_here") # should not error + p.remove_all_quantities() + p.remove_all_quantities() + + ps.remove_all_structures() + + def test_scalar_isosurface_array(self): + node_dim = (10,12,14) + + p = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), node_dim) + vals = np.random.rand(*node_dim) + + p.add_scalar_quantity("test_vals", vals, defined_on='nodes', enabled=True, + enable_gridcube_viz=False, enable_isosurface_viz=True, + isosurface_level=-0.2, isosurface_color=(0.5,0.6,0.7), + slice_planes_affect_isosurface=False, + register_isosurface_as_mesh_with_name="isomesh") + + ps.remove_all_structures() + + def test_scalar_isosurface_callable(self): + node_dim = (10,12,14) + + p = ps.register_volume_grid("test_grid", (0.,0.,0,), (1., 1., 1.), node_dim) + def sphere_sdf(pts): return np.linalg.norm(pts, axis=-1) - 1. + + p.add_scalar_quantity_from_callable("test_vals", sphere_sdf, defined_on='nodes', enabled=True, + enable_gridcube_viz=False, enable_isosurface_viz=True, + isosurface_level=-0.2, isosurface_color=(0.5,0.6,0.7), + slice_planes_affect_isosurface=False, + register_isosurface_as_mesh_with_name="isomesh") + + ps.remove_all_structures() + + + +class TestCameraView(unittest.TestCase): + + def generate_parameters(self): + intrinsics = ps.CameraIntrinsics(fov_vertical_deg=60, aspect=2) + extrinsics = ps.CameraExtrinsics(root=(2., 2., 2.), look_dir=(-1., -1.,-1.), up_dir=(0.,1.,0.)) + return ps.CameraParameters(intrinsics, extrinsics) + + def test_add_remove(self): + + # add + cam = ps.register_camera_view("cam1", self.generate_parameters()) + self.assertTrue(ps.has_camera_view("cam1")) + self.assertFalse(ps.has_camera_view("nope")) + + # remove by name + ps.register_camera_view("cam2", self.generate_parameters()) + ps.remove_camera_view("cam2") + self.assertTrue(ps.has_camera_view("cam1")) + self.assertFalse(ps.has_camera_view("cam2")) + + # remove by ref + c = ps.register_camera_view("cam3", self.generate_parameters()) + c.remove() + self.assertTrue(ps.has_camera_view("cam1")) + self.assertFalse(ps.has_camera_view("cam3")) + + # get by name + ps.register_camera_view("cam3", self.generate_parameters()) + p = ps.get_camera_view("cam3") # should be wrapped instance, not underlying PSB instance + self.assertTrue(isinstance(p, ps.CameraView)) + + ps.remove_all_structures() + + def test_render(self): + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + ps.show(3) + ps.remove_all_structures() + + def test_transform(self): + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + test_transforms(self, cam) + ps.remove_all_structures() + + def test_options(self): + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + # widget color + color = (0.3, 0.3, 0.5) + cam.set_widget_color(color) + ret_color = cam.get_widget_color() + for i in range(3): + self.assertAlmostEqual(ret_color[i], color[i]) + + # widget thickness + cam.set_widget_thickness(0.03) + self.assertAlmostEqual(0.03, cam.get_widget_thickness()) + + # widget focal length + cam.set_widget_focal_length(0.03, False) + self.assertAlmostEqual(0.03, cam.get_widget_focal_length()) + + ps.show(3) + ps.remove_all_structures() + + def test_update(self): + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + cam.update_camera_parameters(self.generate_parameters()) + + ps.show(3) + ps.remove_all_structures() + + def test_camera_things(self): + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + cam.set_view_to_this_camera() + ps.show(3) + cam.set_view_to_this_camera(with_flight=True) + ps.show(3) + + ps.remove_all_structures() + + def test_camera_parameters(self): + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + params = cam.get_camera_parameters() + + self.assertTrue(isinstance(params.get_intrinsics(), ps.CameraIntrinsics)) + self.assertTrue(isinstance(params.get_extrinsics(), ps.CameraExtrinsics)) + + assertArrayWithShape(self, params.get_R(), [3,3]) + assertArrayWithShape(self, params.get_T(), [3]) + assertArrayWithShape(self, params.get_view_mat(), [4,4]) + assertArrayWithShape(self, params.get_E(), [4,4]) + assertArrayWithShape(self, params.get_position(), [3]) + assertArrayWithShape(self, params.get_look_dir(), [3]) + assertArrayWithShape(self, params.get_up_dir(), [3]) + assertArrayWithShape(self, params.get_right_dir(), [3]) + assertArrayWithShape(self, params.get_camera_frame()[0], [3]) + assertArrayWithShape(self, params.get_camera_frame()[1], [3]) + assertArrayWithShape(self, params.get_camera_frame()[2], [3]) + + self.assertTrue(isinstance(params.get_fov_vertical_deg(), float)) + self.assertTrue(isinstance(params.get_aspect(), float)) + + rays = params.generate_camera_rays((300,200)) + ray_corners = params.generate_camera_ray_corners() + + def test_floating_scalar_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + cam.add_scalar_image_quantity("scalar_img", np.zeros((dimX, dimY))) + cam.add_scalar_image_quantity("scalar_img2", np.zeros((dimX, dimY)), enabled=True, image_origin='lower_left', datatype='symmetric', vminmax=(-3.,.3), cmap='reds', show_in_camera_billboard=True) + cam.add_scalar_image_quantity("scalar_img3", np.zeros((dimX, dimY)), enabled=True, show_in_imgui_window=True, show_in_camera_billboard=False) + cam.add_scalar_image_quantity("scalar_img4", np.zeros((dimX, dimY)), enabled=True, show_fullscreen=True, show_in_camera_billboard=False, transparency=0.5) + + # true floating adder + ps.add_scalar_image_quantity("scalar_img2", np.zeros((dimX, dimY)), enabled=True, image_origin='lower_left', datatype='symmetric', vminmax=(-3.,.3), cmap='reds') + + ps.show(3) + ps.remove_all_structures() + + def test_floating_color_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + cam.add_color_image_quantity("color_img", np.zeros((dimX, dimY, 3))) + cam.add_color_image_quantity("color_img2", np.zeros((dimX, dimY, 3)), enabled=True, image_origin='lower_left', show_in_camera_billboard=True) + cam.add_color_image_quantity("color_img3", np.zeros((dimX, dimY, 3)), enabled=True, show_in_imgui_window=True, show_in_camera_billboard=False) + cam.add_color_image_quantity("color_img4", np.zeros((dimX, dimY, 3)), enabled=True, show_fullscreen=True, show_in_camera_billboard=False, transparency=0.5) + + # true floating adder + ps.add_color_image_quantity("color_img2", np.zeros((dimX, dimY, 3)), enabled=True, image_origin='lower_left', show_in_camera_billboard=False) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_color_alpha_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + cam.add_color_alpha_image_quantity("color_alpha_img", np.zeros((dimX, dimY, 4))) + cam.add_color_alpha_image_quantity("color_alpha_img2", np.zeros((dimX, dimY, 4)), enabled=True, image_origin='lower_left', show_in_camera_billboard=True) + cam.add_color_alpha_image_quantity("color_alpha_img3", np.zeros((dimX, dimY, 4)), enabled=True, show_in_imgui_window=True, show_in_camera_billboard=False, is_premultiplied=True) + cam.add_color_alpha_image_quantity("color_alpha_img4", np.zeros((dimX, dimY, 4)), enabled=True, show_fullscreen=True, show_in_camera_billboard=False) + + # true floating adder + ps.add_color_alpha_image_quantity("color_alpha_img3", np.zeros((dimX, dimY, 4)), enabled=True, show_in_imgui_window=True, show_in_camera_billboard=False) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_depth_render_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + depths = np.zeros((dimX, dimY)) + normals = np.ones((dimX, dimY, 3)) + + cam.add_depth_render_image_quantity("render_img", depths, normals) + cam.add_depth_render_image_quantity("render_img2", depths, normals, enabled=True, image_origin='lower_left', color=(0., 1., 0.), material='wax', transparency=0.7) + + # true floating adder + ps.add_depth_render_image_quantity("render_img3", depths, normals, enabled=True, image_origin='lower_left', color=(0., 1., 0.), material='wax', transparency=0.7, allow_fullscreen_compositing=True) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_color_render_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + depths = np.zeros((dimX, dimY)) + normals = np.ones((dimX, dimY, 3)) + colors = np.ones((dimX, dimY, 3)) + + cam.add_color_render_image_quantity("render_img", depths, normals, colors) + cam.add_color_render_image_quantity("render_img2", depths, normals, colors, enabled=True, image_origin='lower_left', material='wax', transparency=0.7) + + # true floating adder + ps.add_color_render_image_quantity("render_img3", depths, normals, colors, enabled=True, image_origin='lower_left', material='wax', transparency=0.7, allow_fullscreen_compositing=True) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_scalar_render_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + depths = np.zeros((dimX, dimY)) + normals = np.ones((dimX, dimY, 3)) + scalars = np.ones((dimX, dimY)) + + cam.add_scalar_render_image_quantity("render_img", depths, normals, scalars) + cam.add_scalar_render_image_quantity("render_img2", depths, normals, scalars, enabled=True, image_origin='lower_left', material='wax', transparency=0.7) + + # true floating adder + ps.add_scalar_render_image_quantity("render_img3", depths, normals, scalars, enabled=True, image_origin='lower_left', material='wax', transparency=0.7, allow_fullscreen_compositing=True) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_raw_color_render_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + depths = np.zeros((dimX, dimY)) + colors = np.ones((dimX, dimY, 3)) + + cam.add_raw_color_render_image_quantity("render_img", depths, colors) + cam.add_raw_color_render_image_quantity("render_img2", depths, colors, enabled=True, image_origin='lower_left', transparency=0.7) + + # true floating adder + ps.add_raw_color_render_image_quantity("render_img3", depths, colors, enabled=True, image_origin='lower_left', transparency=0.7, allow_fullscreen_compositing=True) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_raw_color_alpha_render_images(self): + + # technically these can be added to any structure, but we will test them here + + cam = ps.register_camera_view("cam1", self.generate_parameters()) + + dimX = 300 + dimY = 600 + + depths = np.zeros((dimX, dimY)) + colors = np.ones((dimX, dimY, 4)) + + cam.add_raw_color_alpha_render_image_quantity("render_img", depths, colors) + cam.add_raw_color_alpha_render_image_quantity("render_img2", depths, colors, enabled=True, image_origin='lower_left', transparency=0.7, is_premultiplied=True) + + # true floating adder + ps.add_raw_color_alpha_render_image_quantity("render_img3", depths, colors, enabled=True, image_origin='lower_left', transparency=0.7, allow_fullscreen_compositing=True) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_implicit_surface_render(self): + + # this can be called free-floating or on a camera view, but we will test them here + + def sphere_sdf(pts): return np.linalg.norm(pts, axis=-1) - 1. + + # basic + ps.render_implicit_surface("sphere sdf", sphere_sdf, 'fixed_step', subsample_factor=10, n_max_steps=10, enabled=True) + + # with some args + ps.render_implicit_surface("sphere sdf", sphere_sdf, 'sphere_march', enabled=True, + camera_parameters=self.generate_parameters(), + dim=(50,75), + miss_dist=20.5, miss_dist_relative=True, + hit_dist=0.001, hit_dist_relative=False, + step_factor=0.98, + normal_sample_eps=0.02, + step_size=0.01, step_size_relative=True, + n_max_steps=50, + material='wax', + color=(0.5,0.5,0.5) + ) + + # from this camera view + cam = ps.register_camera_view("cam1", self.generate_parameters()) + ps.render_implicit_surface("sphere sdf", sphere_sdf, 'sphere_march', dim=(50,75), n_max_steps=10, camera_view=cam, enabled=True) + + ps.show(3) + ps.remove_all_structures() + + def test_floating_implicit_surface_color_render(self): + + # this can be called free-floating or on a camera view, but we will test them here + + def sphere_sdf(pts): return np.linalg.norm(pts, axis=-1) - 1. + def color_func(pts): return np.zeros_like(pts) + + # basic + ps.render_implicit_surface_color("sphere sdf", sphere_sdf, color_func, 'fixed_step', subsample_factor=10, n_max_steps=10, enabled=True) + + # with some args + ps.render_implicit_surface_color("sphere sdf", sphere_sdf, color_func, 'sphere_march', enabled=True, + camera_parameters=self.generate_parameters(), + dim=(50,75), + miss_dist=20.5, miss_dist_relative=True, + hit_dist=0.001, hit_dist_relative=False, + step_factor=0.98, + normal_sample_eps=0.02, + step_size=0.01, step_size_relative=True, + n_max_steps=50, + material='wax', + ) + + # from this camera view + cam = ps.register_camera_view("cam1", self.generate_parameters()) + ps.render_implicit_surface_color("sphere sdf", sphere_sdf, color_func, 'sphere_march', dim=(50,75), n_max_steps=10, camera_view=cam, enabled=True) + + ps.show(3) + ps.remove_all_structures() + + + def test_floating_implicit_surface_scalar_render(self): + + # this can be called free-floating or on a camera view, but we will test them here + + def sphere_sdf(pts): return np.linalg.norm(pts, axis=-1) - 1. + def scalar_func(pts): return np.ones_like(pts[:,0]) + + # basic + ps.render_implicit_surface_scalar("sphere sdf", sphere_sdf, scalar_func, 'fixed_step', subsample_factor=10, n_max_steps=10, enabled=True) + + # with some args + ps.render_implicit_surface_scalar("sphere sdf", sphere_sdf, scalar_func, 'sphere_march', enabled=True, + camera_parameters=self.generate_parameters(), + dim=(50,75), + miss_dist=20.5, miss_dist_relative=True, + hit_dist=0.001, hit_dist_relative=False, + step_factor=0.98, + normal_sample_eps=0.02, + step_size=0.01, step_size_relative=True, + n_max_steps=50, + material='wax', + cmap='blues', + vminmax=(0.,1.) + ) + + # from this camera view + cam = ps.register_camera_view("cam1", self.generate_parameters()) + ps.render_implicit_surface_scalar("sphere sdf", sphere_sdf, scalar_func, 'sphere_march', dim=(50,75), n_max_steps=10, camera_view=cam, enabled=True) + + ps.show(3) + ps.remove_all_structures() + +class TestManagedBuffers(unittest.TestCase): + + def test_managed_buffer_basics(self): + + # NOTE: this only tests the float & vec3 versions, really there are variant methods for each type + + def generate_points(n_pts=10): + np.random.seed(777) + return np.random.rand(n_pts, 3) + + def generate_scalar(n_pts=10): + np.random.seed(777) + return np.random.rand(n_pts) + + + # create a dummy point cloud; + ps_cloud = ps.register_point_cloud("test_cloud", generate_points()) + ps_scalar = ps_cloud.add_scalar_quantity("test_vals", generate_scalar()) + ps.show(3) + + # test a structure buffer of vec3 + pos_buf = ps_cloud.get_buffer("points") + self.assertEqual(pos_buf.size(), 10) + self.assertTrue(pos_buf.has_data()) + pos_buf.summary_string() + pos_buf.get_value(3) + pos_buf.update_data(generate_points()) + + # test a quantity buffer of float + scalar_buf = ps_cloud.get_quantity_buffer("test_vals", "values") + self.assertEqual(scalar_buf.size(), 10) + self.assertTrue(scalar_buf.has_data()) + scalar_buf.summary_string() + scalar_buf.get_value(3) + scalar_buf.update_data(generate_scalar()) + + ps.show(3) + + # test a free-floating quantity buffer + dimX = 200 + dimY = 300 + ps.add_scalar_image_quantity("test_float_img", np.zeros((dimX, dimY))) + img_buf = ps.get_quantity_buffer("test_float_img", "values") + self.assertEqual(img_buf.size(), dimX*dimY) + self.assertTrue(img_buf.has_data()) + img_buf.summary_string() + img_buf.get_value(3) + img_buf.update_data(np.zeros(dimX*dimY)) + + + ps.show(3) + + if __name__ == '__main__': # Parse out test-specific args (this is kinda poor design, but very useful)