diff --git a/VERSION b/VERSION index a660a47..6a40caf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5_3_0 +5_3_1 diff --git a/blender_manifest.toml b/blender_manifest.toml index 9a54871..badf1c9 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -2,7 +2,7 @@ schema_version = "1.0.0" id = "modular_tree" name = "Modular Tree" -version = "5.3.0" +version = "5.3.1" website = "https://github.com/GoodPie/modular_tree" tagline = "Procedural node based 3D tree generation" maintainer = "GoodPie " diff --git a/m_tree/CMakeLists.txt b/m_tree/CMakeLists.txt index df364e3..97bb137 100644 --- a/m_tree/CMakeLists.txt +++ b/m_tree/CMakeLists.txt @@ -1,6 +1,6 @@ -cmake_minimum_required(VERSION 3.5...3.27) +cmake_minimum_required(VERSION 3.15...3.31) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/m_tree/dependencies/pybind11 b/m_tree/dependencies/pybind11 index a2e59f0..f5fbe86 160000 --- a/m_tree/dependencies/pybind11 +++ b/m_tree/dependencies/pybind11 @@ -1 +1 @@ -Subproject commit a2e59f0e7065404b44dfe92a28aca47ba1378dc4 +Subproject commit f5fbe867d2d26e4a0a9177a51f6e568868ad3dc8 diff --git a/m_tree/install.py b/m_tree/install.py index 53f4a5e..81b6452 100644 --- a/m_tree/install.py +++ b/m_tree/install.py @@ -46,7 +46,7 @@ def build(): if not os.path.exists(build_dir): os.makedirs(build_dir) - subprocess.check_call(["cmake", "../", "-DCMAKE_POLICY_VERSION_MINIMUM=3.5"], cwd=build_dir) + subprocess.check_call(["cmake", "../"], cwd=build_dir) subprocess.check_call(["cmake", "--build", ".", "--config", "Release"], cwd=build_dir) print([i for i in os.listdir(os.path.join(os.path.dirname(__file__), "binaries"))]) diff --git a/m_tree/pyproject.toml b/m_tree/pyproject.toml index c7c1b01..740fee5 100644 --- a/m_tree/pyproject.toml +++ b/m_tree/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "m_tree" -version = "5.3.0" +version = "5.3.1" description = "Procedural 3D tree generation library" requires-python = ">=3.11" diff --git a/m_tree/python_bindings/CMakeLists.txt b/m_tree/python_bindings/CMakeLists.txt index c26175b..121a2c6 100644 --- a/m_tree/python_bindings/CMakeLists.txt +++ b/m_tree/python_bindings/CMakeLists.txt @@ -1,6 +1,6 @@ -cmake_minimum_required(VERSION 3.5...3.27) +cmake_minimum_required(VERSION 3.15...3.31) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/m_tree/source/CMakeLists.txt b/m_tree/source/CMakeLists.txt index caee4fc..d702330 100644 --- a/m_tree/source/CMakeLists.txt +++ b/m_tree/source/CMakeLists.txt @@ -1,6 +1,6 @@ -cmake_minimum_required(VERSION 3.5...3.27) +cmake_minimum_required(VERSION 3.15...3.31) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -fPIC") diff --git a/m_tree/source/meshers/base_types/TreeMesher.hpp b/m_tree/source/meshers/base_types/TreeMesher.hpp index 2d0c7e0..7f1f57c 100644 --- a/m_tree/source/meshers/base_types/TreeMesher.hpp +++ b/m_tree/source/meshers/base_types/TreeMesher.hpp @@ -1,9 +1,16 @@ #pragma once #include "source/mesh/Mesh.hpp" #include "source/tree/Tree.hpp" +#include namespace Mtree { + +template +concept Mesher = requires(T& mesher, Tree& tree) { + { mesher.mesh_tree(tree) } -> std::same_as; +}; + class TreeMesher { public: diff --git a/m_tree/source/meshers/manifold_mesher/ManifoldMesher.cpp b/m_tree/source/meshers/manifold_mesher/ManifoldMesher.cpp index 70cefde..5cd36cf 100644 --- a/m_tree/source/meshers/manifold_mesher/ManifoldMesher.cpp +++ b/m_tree/source/meshers/manifold_mesher/ManifoldMesher.cpp @@ -4,6 +4,7 @@ #include "source/utilities/NodeUtilities.hpp" #include #include +#include using namespace Mtree; using namespace Mtree::NodeUtilities; @@ -81,7 +82,7 @@ CircleDesignator add_circle(const Vector3& node_position, const Node& node, floa for (size_t i = 0; i < radial_n_points; i++) { - float angle = (float)i / radial_n_points * 2 * M_PI; + float angle = (float)i / radial_n_points * 2 * std::numbers::pi_v; Vector3 point = cos(angle) * right + sin(angle) * up; point = point * radius + circle_position; int index = mesh.add_vertex(point); @@ -96,7 +97,8 @@ CircleDesignator add_circle(const Vector3& node_position, const Node& node, floa mesh.uvs.emplace_back((float)i / radial_n_points, uv_y); } mesh.uvs.emplace_back(1, uv_y); - return CircleDesignator{vertex_index, uv_index, radial_n_points}; + return CircleDesignator{ + .vertex_index = vertex_index, .uv_index = uv_index, .radial_n = radial_n_points}; } bool is_index_in_branch_mask(const std::vector& mask, const int index, @@ -149,17 +151,22 @@ float get_branch_angle_around_parent(const Node& parent, const Node& branch) Vector3 up = right.cross(parent.direction); float cos_angle = projected_branch_dir.dot(right); float sin_angle = projected_branch_dir.dot(up); - return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * M_PI, 2 * M_PI); + return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * std::numbers::pi_v, + 2 * std::numbers::pi_v); } IndexRange get_branch_indices_on_circle(const int radial_n_points, const float circle_radius, const float branch_radius, const float branch_angle) { float angle_delta = std::asin(std::clamp(branch_radius / circle_radius, -1.f, 1.f)); - float increment = 2 * M_PI / radial_n_points; - int min_index = (int)(std::fmod(branch_angle - angle_delta + 2 * M_PI, 2 * M_PI) / increment); + float increment = 2 * std::numbers::pi_v / radial_n_points; + int min_index = (int)(std::fmod(branch_angle - angle_delta + 2 * std::numbers::pi_v, + 2 * std::numbers::pi_v) / + increment); int max_index = - (int)(std::fmod(branch_angle + angle_delta + increment + 2 * M_PI, 2 * M_PI) / increment); + (int)(std::fmod(branch_angle + angle_delta + increment + 2 * std::numbers::pi_v, + 2 * std::numbers::pi_v) / + increment); return IndexRange{min_index, max_index}; } @@ -265,14 +272,15 @@ float get_child_twist(const Node& child, const Node& parent) Vector3 up = right.cross(child.direction); float cos_angle = child.tangent.dot(right); float sin_angle = child.tangent.dot(up); - return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * M_PI, 2 * M_PI); + return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * std::numbers::pi_v, + 2 * std::numbers::pi_v); } int add_child_base_uvs(float parent_uv_y, const Node& parent, const NodeChild& child, const IndexRange child_range, const int child_radial_n, const int parent_radial_n, Mesh& mesh) { - float uv_growth = parent.length / (parent.radius + .001f) / (2 * M_PI); + float uv_growth = parent.length / (parent.radius + .001f) / (2 * std::numbers::pi_v); for (size_t i = 0; i < 2; i++) // recreating outer uvs (but without continuous (no looping back to x=0) { @@ -292,7 +300,8 @@ int add_child_base_uvs(float parent_uv_y, const Node& parent, const NodeChild& c float uv_circle_radius = std::min((float)child_radial_n / parent_radial_n, uv_growth / 2) * .6f; for (size_t i = 0; i < child_radial_n; i++) // inner uvs { - float angle = (float)i / (child_radial_n - 1) * 2 * M_PI + M_PI; + float angle = (float)i / (child_radial_n - 1) * 2 * std::numbers::pi_v + + std::numbers::pi_v; Vector2 uv_position = Vector2{cos(angle), sin(angle)} * uv_circle_radius + uv_circle_center; mesh.uvs.push_back(uv_position); } @@ -322,9 +331,9 @@ CircleDesignator add_child_circle(const Node& parent, const NodeChild& child, get_child_index_order(parent_base, child_radial_n, child_range, child, parent, mesh); float child_twist = get_child_twist(child.node, parent); - int offset = - (int)(child_twist / (2 * M_PI) * child_radial_n - child_radial_n / 4 + child_radial_n) % - child_radial_n; + int offset = (int)(child_twist / (2 * std::numbers::pi_v)*child_radial_n - + child_radial_n / 4 + child_radial_n) % + child_radial_n; CircleDesignator child_base{(int)mesh.vertices.size(), (int)mesh.uvs.size(), child_radial_n}; child_base.uv_index = add_child_base_uvs(uv_y, parent, child, child_range, child_radial_n, @@ -357,11 +366,12 @@ bool has_side_branches(const Node& node) } void mesh_node_rec(const Node& node, const Vector3& node_position, const CircleDesignator& base, - Mesh& mesh, const float uv_y, const PivotPainterContext& pp_ctx, int& stem_id_counter) + Mesh& mesh, const float uv_y, const PivotPainterContext& pp_ctx, + int& stem_id_counter) { if (node.children.size() < 2) { - float uv_growth = node.length / (node.radius + .001f) / (2 * M_PI); + float uv_growth = node.length / (node.radius + .001f) / (2 * std::numbers::pi_v); auto child_circle = add_circle(node_position, node, 1, base.radial_n, mesh, uv_y + uv_growth, pp_ctx); bridge_circles(base, child_circle, base.radial_n, mesh); @@ -375,8 +385,9 @@ void mesh_node_rec(const Node& node, const Vector3& node_position, const CircleD } else { - float uv_growth = node.length / (node.radius + .001f) / (2 * M_PI); - auto end_circle = add_circle(node_position, node, 1, base.radial_n, mesh, uv_y + uv_growth, pp_ctx); + float uv_growth = node.length / (node.radius + .001f) / (2 * std::numbers::pi_v); + auto end_circle = + add_circle(node_position, node, 1, base.radial_n, mesh, uv_y + uv_growth, pp_ctx); std::vector children_ranges = get_children_ranges(node, base.radial_n); bridge_circles(base, end_circle, base.radial_n, mesh, &children_ranges); for (int i = 0; i < node.children.size(); i++) @@ -402,10 +413,11 @@ void mesh_node_rec(const Node& node, const Vector3& node_position, const CircleD child_pp_ctx.pivot_position = child_pos; child_pp_ctx.branch_extent = calculate_branch_extent(child.node); - auto child_base = add_child_circle(node, child, child_pos, node_position, base, - children_ranges[i - 1], uv_y, mesh, child_pp_ctx); - mesh_node_rec(node.children[i]->node, child_pos, child_base, mesh, - uv_y + uv_growth, child_pp_ctx, stem_id_counter); + auto child_base = + add_child_circle(node, child, child_pos, node_position, base, + children_ranges[i - 1], uv_y, mesh, child_pp_ctx); + mesh_node_rec(node.children[i]->node, child_pos, child_base, mesh, uv_y + uv_growth, + child_pp_ctx, stem_id_counter); } } } diff --git a/m_tree/source/meshers/manifold_mesher/ManifoldMesher.hpp b/m_tree/source/meshers/manifold_mesher/ManifoldMesher.hpp index 1df0b85..84cd491 100644 --- a/m_tree/source/meshers/manifold_mesher/ManifoldMesher.hpp +++ b/m_tree/source/meshers/manifold_mesher/ManifoldMesher.hpp @@ -1,5 +1,6 @@ #pragma once #include "../base_types/TreeMesher.hpp" +#include #include namespace Mtree @@ -10,14 +11,14 @@ class ManifoldMesher : public TreeMesher public: struct AttributeNames { - inline static std::string smooth_amount = "smooth_amount"; - inline static std::string radius = "radius"; - inline static std::string direction = "direction"; + inline static const std::string smooth_amount = "smooth_amount"; + inline static const std::string radius = "radius"; + inline static const std::string direction = "direction"; // Pivot Painter 2.0 attributes - inline static std::string stem_id = "stem_id"; - inline static std::string hierarchy_depth = "hierarchy_depth"; - inline static std::string pivot_position = "pivot_position"; - inline static std::string branch_extent = "branch_extent"; + inline static const std::string stem_id = "stem_id"; + inline static const std::string hierarchy_depth = "hierarchy_depth"; + inline static const std::string pivot_position = "pivot_position"; + inline static const std::string branch_extent = "branch_extent"; }; int radial_resolution = 8; diff --git a/m_tree/source/tree/GrowthInfo.hpp b/m_tree/source/tree/GrowthInfo.hpp index 87258a3..fcd9086 100644 --- a/m_tree/source/tree/GrowthInfo.hpp +++ b/m_tree/source/tree/GrowthInfo.hpp @@ -1,10 +1,50 @@ #pragma once +#include +#include namespace Mtree { -class GrowthInfo +using Vector3 = Eigen::Vector3f; + +struct BranchGrowthInfo { - public: - virtual ~GrowthInfo() {} + float desired_length; + float origin_radius; + Vector3 position; + float current_length = 0; + float deviation_from_rest_pose = 0; + float cumulated_weight = 0; + float age = 0; + bool inactive = false; }; + +struct BioNodeInfo +{ + enum class NodeType + { + Meristem, + Branch, + Cut, + Ignored, + Dormant, + Flower + } type; + float branch_weight = 0; + Vector3 center_of_mass; + Vector3 absolute_position; + float vigor_ratio = 1; + float vigor = 0; + int age = 0; + float philotaxis_angle = 0; + bool is_lateral = false; + + BioNodeInfo(NodeType type = NodeType::Ignored, int age = 0, float philotaxis_angle = 0, + bool is_lateral = false) + : type(type), age(age), philotaxis_angle(philotaxis_angle), is_lateral(is_lateral) + { + } +}; + +using GrowthInfo = std::variant; + } // namespace Mtree diff --git a/m_tree/source/tree/Node.hpp b/m_tree/source/tree/Node.hpp index bb2a586..f91d6e5 100644 --- a/m_tree/source/tree/Node.hpp +++ b/m_tree/source/tree/Node.hpp @@ -19,7 +19,7 @@ class Node float length; float radius; int creator_id = 0; - std::unique_ptr growthInfo = nullptr; + GrowthInfo growthInfo; bool is_leaf() const; diff --git a/m_tree/source/tree_functions/BranchFunction.cpp b/m_tree/source/tree_functions/BranchFunction.cpp index d98cc96..6a32d66 100644 --- a/m_tree/source/tree_functions/BranchFunction.cpp +++ b/m_tree/source/tree_functions/BranchFunction.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include "BranchFunction.hpp" #include "source/utilities/GeometryUtilities.hpp" @@ -13,7 +15,7 @@ constexpr float EPSILON = 0.001f; void update_positions_rec(Node& node, const Vector3& position) { - auto& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); info.position = position; for (auto& child : node.children) @@ -67,23 +69,19 @@ Vector3 get_split_direction(const Node& parent, const Vector3& parent_position, void mark_inactive(Node& node) { - auto& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); info.inactive = true; } bool propagate_inactive_rec(Node& node) { - auto* info = dynamic_cast(node.growthInfo.get()); + auto* info = std::get_if(&node.growthInfo); if (node.children.size() == 0 || info->inactive) return info->inactive; - bool inactive = false; - for (size_t i = 0; i < node.children.size(); i++) - { - if (propagate_inactive_rec(node.children[i]->node)) - inactive = true; - } + bool inactive = std::ranges::any_of(node.children, [](const auto& child) + { return propagate_inactive_rec(child->node); }); info->inactive = inactive; return inactive; } @@ -96,20 +94,21 @@ void BranchFunction::apply_gravity_to_branch(Node& branch_origin) propagate_inactive_rec(branch_origin); update_weight_rec(branch_origin); apply_gravity_rec(branch_origin, Eigen::AngleAxisf::Identity()); - BranchGrowthInfo& info = static_cast(*branch_origin.growthInfo); + auto& info = std::get(branch_origin.growthInfo); update_positions_rec(branch_origin, info.position); } void BranchFunction::apply_gravity_rec(Node& node, Eigen::AngleAxisf curent_rotation) { - BranchGrowthInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); if (!info.inactive || true) { - float horizontality = 1 - abs(node.direction.z()); + float horizontality = 1 - std::abs(node.direction.z()); info.age += 1 / resolution; float displacement = horizontality * std::pow(info.cumulated_weight, .5f) * gravity->strength / resolution / resolution / 1000 / (1 + info.age); - displacement *= std::exp(-std::abs(info.deviation_from_rest_pose / resolution * gravity->stiffness)); + displacement *= + std::exp(-std::abs(info.deviation_from_rest_pose / resolution * gravity->stiffness)); info.deviation_from_rest_pose += displacement; Vector3 tangent = node.direction.cross(Vector3{0, 0, -1}).normalized(); @@ -131,11 +130,11 @@ void BranchFunction::update_weight_rec(Node& node) for (auto& child : node.children) { update_weight_rec(child->node); - BranchGrowthInfo& child_info = dynamic_cast(*child->node.growthInfo); + auto& child_info = std::get(child->node.growthInfo); node_weight += child_info.cumulated_weight; } - BranchGrowthInfo* info = dynamic_cast(node.growthInfo.get()); + auto* info = std::get_if(&node.growthInfo); info->cumulated_weight = node_weight; } @@ -150,16 +149,16 @@ void BranchFunction::grow_node_once(Node& node, const int id, return; } - BranchGrowthInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); float factor_in_branch = info.current_length / info.desired_length; float child_radius = Geometry::lerp(info.origin_radius, info.origin_radius * end_radius, factor_in_branch); float child_length = std::min(1 / resolution, info.desired_length - info.current_length); bool should_terminate; - Vector3 child_direction = get_main_child_direction(node, info.position, gravity->up_attraction, flatness, - randomness.execute(factor_in_branch), - resolution, should_terminate); + Vector3 child_direction = get_main_child_direction( + node, info.position, gravity->up_attraction, flatness, randomness.execute(factor_in_branch), + resolution, should_terminate); if (should_terminate) { @@ -167,38 +166,42 @@ void BranchFunction::grow_node_once(Node& node, const int id, return; } - NodeChild child{Node{child_direction, node.tangent, child_length, child_radius, id}, 1}; + NodeChild child{.node = Node{child_direction, node.tangent, child_length, child_radius, id}, + .position_in_parent = 1}; node.children.push_back(std::make_shared(std::move(child))); auto& child_node = node.children.back()->node; float current_length = info.current_length + child_length; Vector3 child_position = info.position + child_direction * child_length; - BranchGrowthInfo child_info{info.desired_length, info.origin_radius, child_position, - current_length}; - child_node.growthInfo = std::make_unique(child_info); + child_node.growthInfo = BranchGrowthInfo{.desired_length = info.desired_length, + .origin_radius = info.origin_radius, + .position = child_position, + .current_length = current_length}; if (current_length < info.desired_length) { results.push(std::ref(child_node)); } - bool do_split = - rand_gen.get_0_1() * resolution < split->probability; // should the node split into two children + bool do_split = rand_gen.get_0_1() * resolution < + split->probability; // should the node split into two children if (do_split) { - Vector3 split_child_direction = get_split_direction(node, info.position, gravity->up_attraction, - flatness, resolution, split->angle); + Vector3 split_child_direction = get_split_direction( + node, info.position, gravity->up_attraction, flatness, resolution, split->angle); float split_child_radius = node.radius * split->radius; NodeChild child{ - Node{split_child_direction, node.tangent, child_length, split_child_radius, id}, - rand_gen.get_0_1()}; + .node = Node{split_child_direction, node.tangent, child_length, split_child_radius, id}, + .position_in_parent = rand_gen.get_0_1()}; node.children.push_back(std::make_shared(std::move(child))); auto& child_node = node.children.back()->node; Vector3 split_child_position = info.position + split_child_direction * child_length; - BranchGrowthInfo child_info{info.desired_length, info.origin_radius * split->radius, - split_child_position, current_length}; - child_node.growthInfo = std::make_unique(child_info); + child_node.growthInfo = + BranchGrowthInfo{.desired_length = info.desired_length, + .origin_radius = info.origin_radius * split->radius, + .position = split_child_position, + .current_length = current_length}; if (current_length < info.desired_length) { results.push(std::ref(child_node)); @@ -249,7 +252,8 @@ BranchFunction::get_origins(std::vector& stems, const int id, const int pa float crown_start_z = effective_crown_height * crown->base_size; float crown_zone_height = effective_crown_height * (1.0f - crown->base_size); - float origins_dist = 1 / (distribution->density + .001); // distance between two consecutive origins + float origins_dist = + 1 / (distribution->density + .001); // distance between two consecutive origins for (auto& branch : selection) // parent branches { @@ -260,9 +264,10 @@ BranchFunction::get_origins(std::vector& stems, const int id, const int pa float branch_length = NodeUtilities::get_branch_length(*branch[0].node); float absolute_start = - distribution->start * branch_length; // the length at which we can start adding new branch origins - float absolute_end = - distribution->end * branch_length; // the length at which we stop adding new branch origins + distribution->start * + branch_length; // the length at which we can start adding new branch origins + float absolute_end = distribution->end * + branch_length; // the length at which we stop adding new branch origins float current_length = 0; float dist_to_next_origin = absolute_start; Vector3 tangent = Geometry::get_orthogonal_vector(branch[0].node->direction); @@ -276,8 +281,10 @@ BranchFunction::get_origins(std::vector& stems, const int id, const int pa { continue; } - auto rot = Eigen::AngleAxisf((distribution->phillotaxis + (rand_gen.get_0_1() - .5) * 2) / 180 * M_PI, - node.direction); + auto rot = + Eigen::AngleAxisf((distribution->phillotaxis + (rand_gen.get_0_1() - .5) * 2) / + 180 * std::numbers::pi_v, + node.direction); if (dist_to_next_origin > node.length) { dist_to_next_origin -= node.length; @@ -311,9 +318,9 @@ BranchFunction::get_origins(std::vector& stems, const int id, const int pa float effective_start_angle = start_angle.execute(factor); // Calculate height-based modifications for crown shape and angle - bool needs_height_calc = crown_zone_height > EPSILON && - (crown->shape != CrownShape::Cylindrical || - std::abs(crown->angle_variation) > EPSILON); + bool needs_height_calc = + crown_zone_height > EPSILON && (crown->shape != CrownShape::Cylindrical || + std::abs(crown->angle_variation) > EPSILON); if (needs_height_calc) { @@ -339,9 +346,10 @@ BranchFunction::get_origins(std::vector& stems, const int id, const int pa { float shape_ratio = CrownShapeUtils::get_shape_ratio( CrownShape::Conical, height_ratio); - float angle_offset = crown->angle_variation * (1.0f - 2.0f * shape_ratio); - effective_start_angle = std::clamp( - effective_start_angle + angle_offset, 0.0f, 180.0f); + float angle_offset = + crown->angle_variation * (1.0f - 2.0f * shape_ratio); + effective_start_angle = + std::clamp(effective_start_angle + angle_offset, 0.0f, 180.0f); } } } @@ -358,9 +366,11 @@ BranchFunction::get_origins(std::vector& stems, const int id, const int pa auto& child_node = node.children.back()->node; Vector3 child_position = node_position + node.direction * node.length * position_in_parent; - child_node.growthInfo = std::make_unique( - branch_length - node_length, child_radius, child_position, - child_node.length, 0); + child_node.growthInfo = + BranchGrowthInfo{.desired_length = branch_length - node_length, + .origin_radius = child_radius, + .position = child_position, + .current_length = child_node.length}; if (branch_length - node_length > 1e-3) origins.push_back(std::ref(child_node)); diff --git a/m_tree/source/tree_functions/BranchFunction.hpp b/m_tree/source/tree_functions/BranchFunction.hpp index 557fd56..9def046 100644 --- a/m_tree/source/tree_functions/BranchFunction.hpp +++ b/m_tree/source/tree_functions/BranchFunction.hpp @@ -14,24 +14,24 @@ namespace Mtree // Parameter groupings for BranchFunction struct SplitParams { - float radius = 0.9f; // Radius multiplier for split branches (0 < x < 1) - float angle = 45.0f; // Angle between split branches (degrees) - float probability = 0.5f; // Probability of a branch splitting (0 < x) + float radius = 0.9f; // Radius multiplier for split branches (0 < x < 1) + float angle = 45.0f; // Angle between split branches (degrees) + float probability = 0.5f; // Probability of a branch splitting (0 < x) }; struct GravityParams { - float strength = 10.0f; // How much branches bend under their weight - float stiffness = 0.1f; // Resistance to bending from gravity - float up_attraction = 0.25f; // Tendency to grow upward (negative values droop) + float strength = 10.0f; // How much branches bend under their weight + float stiffness = 0.1f; // Resistance to bending from gravity + float up_attraction = 0.25f; // Tendency to grow upward (negative values droop) }; struct DistributionParams { - float start = 0.1f; // Position along parent where branches start (0-1) - float end = 1.0f; // Position along parent where branches end (0-1) - float density = 2.0f; // Number of branches per unit length (0 < x) - float phillotaxis = 137.5f; // Spiral angle between branches (degrees) + float start = 0.1f; // Position along parent where branches start (0-1) + float end = 1.0f; // Position along parent where branches end (0-1) + float density = 2.0f; // Number of branches per unit length (0 < x) + float phillotaxis = 137.5f; // Spiral angle between branches (degrees) }; class BranchFunction : public TreeFunction @@ -54,24 +54,6 @@ class BranchFunction : public TreeFunction void execute(std::vector& stems, int id, int parent_id) override; - class BranchGrowthInfo : public GrowthInfo - { - public: - float desired_length; - float current_length; - float origin_radius; - float cumulated_weight = 0; - float deviation_from_rest_pose; - float age = 0; - bool inactive = false; - Vector3 position; - BranchGrowthInfo(float desired_length, float origin_radius, Vector3 position, - float current_length = 0, float deviation = 0) - : desired_length(desired_length), origin_radius(origin_radius), - current_length(current_length), deviation_from_rest_pose(deviation), - position(position) {}; - }; - private: std::vector> get_origins(std::vector& stems, const int id, const int parent_id); diff --git a/m_tree/source/tree_functions/CrownShape.hpp b/m_tree/source/tree_functions/CrownShape.hpp index e0a241b..575f64f 100644 --- a/m_tree/source/tree_functions/CrownShape.hpp +++ b/m_tree/source/tree_functions/CrownShape.hpp @@ -1,14 +1,11 @@ #pragma once #include #include +#include namespace Mtree { -// Cross-platform PI constants (M_PI/M_PI_2 are not available on MSVC) -constexpr float PI = 3.14159265358979323846f; -constexpr float PI_2 = 1.57079632679489661923f; // PI / 2 - enum class CrownShape { Conical = 0, @@ -40,9 +37,9 @@ inline float get_shape_ratio(CrownShape shape, float ratio) case CrownShape::Conical: return MIN_RATIO + RATIO_RANGE * ratio; case CrownShape::Spherical: - return MIN_RATIO + RATIO_RANGE * std::sin(PI * ratio); + return MIN_RATIO + RATIO_RANGE * std::sin(std::numbers::pi_v * ratio); case CrownShape::Hemispherical: - return MIN_RATIO + RATIO_RANGE * std::sin(PI_2 * ratio); + return MIN_RATIO + RATIO_RANGE * std::sin(std::numbers::pi_v / 2 * ratio); case CrownShape::Cylindrical: return 1.0f; case CrownShape::TaperedCylindrical: diff --git a/m_tree/source/tree_functions/GrowthFunction.cpp b/m_tree/source/tree_functions/GrowthFunction.cpp index 978d40e..c392dd9 100644 --- a/m_tree/source/tree_functions/GrowthFunction.cpp +++ b/m_tree/source/tree_functions/GrowthFunction.cpp @@ -1,4 +1,3 @@ -#pragma once #include "GrowthFunction.hpp" #include "./base_types/TreeFunction.hpp" #include "source/utilities/GeometryUtilities.hpp" @@ -17,8 +16,8 @@ void setup_growth_information_rec(Node& node, bool suppress_tip_growth) BioNodeInfo::NodeType tip_type = suppress_tip_growth ? BioNodeInfo::NodeType::Ignored : BioNodeInfo::NodeType::Meristem; - node.growthInfo = std::make_unique( - node.children.size() == 0 ? tip_type : BioNodeInfo::NodeType::Ignored); + node.growthInfo = + BioNodeInfo(node.children.size() == 0 ? tip_type : BioNodeInfo::NodeType::Ignored); for (auto& child : node.children) setup_growth_information_rec(child->node, suppress_tip_growth); } @@ -27,7 +26,7 @@ void setup_growth_information_rec(Node& node, bool suppress_tip_growth) // realtive amount of energy it receive float GrowthFunction::update_vigor_ratio_rec(Node& node) { - BioNodeInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); if (info.type == BioNodeInfo::NodeType::Meristem) { return 1; @@ -55,12 +54,10 @@ float GrowthFunction::update_vigor_ratio_rec(Node& node) float t = apical_dominance; vigor_ratio = (t * light_flux) / (t * light_flux + (1 - t) * child_flux + GrowthConstants::kEpsilon); - static_cast(node.children[i]->node.growthInfo.get())->vigor_ratio = - 1 - vigor_ratio; + std::get(node.children[i]->node.growthInfo).vigor_ratio = 1 - vigor_ratio; light_flux += child_flux; } - static_cast(node.children[0]->node.growthInfo.get())->vigor_ratio = - vigor_ratio; + std::get(node.children[0]->node.growthInfo).vigor_ratio = vigor_ratio; return light_flux; } else @@ -73,16 +70,16 @@ float GrowthFunction::update_vigor_ratio_rec(Node& node) // update the amount of energy available to a node void GrowthFunction::update_vigor_rec(Node& node, float vigor) { - BioNodeInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); info.vigor = vigor; for (auto& child : node.children) { - BioNodeInfo* child_info = static_cast(child->node.growthInfo.get()); - float child_vigor = child_info->vigor_ratio * vigor; + auto& child_info = std::get(child->node.growthInfo); + float child_vigor = child_info.vigor_ratio * vigor; // Give dormant buds a fixed proportion of parent vigor (bypasses competitive apical // dominance) - if (child_info->type == BioNodeInfo::NodeType::Dormant) + if (child_info.type == BioNodeInfo::NodeType::Dormant) { child_vigor = vigor * (1.0f - apical_dominance) * GrowthConstants::kDormantBudVigorFactor; @@ -95,7 +92,7 @@ void GrowthFunction::update_vigor_rec(Node& node, float vigor) // apply rules on the node based on the energy available to it void GrowthFunction::simulate_growth_rec(Node& node, int id) { - BioNodeInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); // Check for dormant bud activation bool activate_dormant = @@ -149,8 +146,7 @@ void GrowthFunction::simulate_growth_rec(Node& node, int id) NodeChild{Node{child_direction, node.tangent, branch_length, child_radius, id}, 1}; float child_angle = split ? info.philotaxis_angle + philotaxis_angle : info.philotaxis_angle; - child.node.growthInfo = - std::make_unique(BioNodeInfo::NodeType::Meristem, 0, child_angle); + child.node.growthInfo = BioNodeInfo(BioNodeInfo::NodeType::Meristem, 0, child_angle); node.children.push_back(std::make_shared(std::move(child))); info.type = BioNodeInfo::NodeType::Branch; } @@ -165,7 +161,7 @@ void GrowthFunction::simulate_growth_rec(Node& node, int id) float child_length = branch_length * (info.vigor + .1f); NodeChild child = NodeChild{Node{child_direction, node.tangent, branch_length, child_radius, id}, 1}; - child.node.growthInfo = std::make_unique(BioNodeInfo::NodeType::Meristem); + child.node.growthInfo = BioNodeInfo(BioNodeInfo::NodeType::Meristem); node.children.push_back(std::make_shared(std::move(child))); info.type = BioNodeInfo::NodeType::Branch; } @@ -177,7 +173,7 @@ void GrowthFunction::simulate_growth_rec(Node& node, int id) void GrowthFunction::get_weight_rec(Node& node) { - BioNodeInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); for (auto& child : node.children) { get_weight_rec(child->node); @@ -188,7 +184,7 @@ void GrowthFunction::get_weight_rec(Node& node) float total_weight = segment_weight; for (auto& child : node.children) { - BioNodeInfo& child_info = static_cast(*child->node.growthInfo); + auto& child_info = std::get(child->node.growthInfo); center_of_mass += child_info.center_of_mass * child_info.branch_weight; total_weight += child_info.branch_weight; } @@ -199,7 +195,7 @@ void GrowthFunction::get_weight_rec(Node& node) void GrowthFunction::apply_gravity_rec(Node& node, Eigen::Matrix3f curent_rotation) { - BioNodeInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); // Only apply gravity bending to growth nodes, not the original trunk if (info.type != BioNodeInfo::NodeType::Ignored) @@ -226,7 +222,7 @@ void GrowthFunction::apply_gravity_rec(Node& node, Eigen::Matrix3f curent_rotati void GrowthFunction::update_absolute_position_rec(Node& node, const Vector3& node_position) { - static_cast(node.growthInfo.get())->absolute_position = node_position; + std::get(node.growthInfo).absolute_position = node_position; for (auto& child : node.children) { Vector3 child_position = @@ -240,7 +236,7 @@ void GrowthFunction::create_lateral_buds_rec(Node& node, int id, Vector3 pos, fl float& current_length, float total_length, float& philo) { - BioNodeInfo& info = static_cast(*node.growthInfo); + auto& info = std::get(node.growthInfo); // Only create buds on Ignored nodes (part of the original trunk structure) if (info.type == BioNodeInfo::NodeType::Ignored && node.children.size() > 0) @@ -284,8 +280,7 @@ void GrowthFunction::create_lateral_buds_rec(Node& node, int id, Vector3 pos, fl NodeChild child{Node{bud_direction, node.tangent, child_length, child_radius, id}, position_in_parent}; - child.node.growthInfo = - std::make_unique(BioNodeInfo::NodeType::Dormant, 0, philo); + child.node.growthInfo = BioNodeInfo(BioNodeInfo::NodeType::Dormant, 0, philo); node.children.push_back(std::make_shared(std::move(child))); dist_to_next = bud_spacing; diff --git a/m_tree/source/tree_functions/GrowthFunction.hpp b/m_tree/source/tree_functions/GrowthFunction.hpp index d4764c3..4eaa53c 100644 --- a/m_tree/source/tree_functions/GrowthFunction.hpp +++ b/m_tree/source/tree_functions/GrowthFunction.hpp @@ -77,34 +77,4 @@ class GrowthFunction : public TreeFunction float& current_length, float total_length, float& philo); }; -class BioNodeInfo : public GrowthInfo -{ - public: - enum class NodeType - { - Meristem, - Branch, - Cut, - Ignored, - Dormant, - Flower - } type; - float branch_weight = 0; - Vector3 center_of_mass; - Vector3 absolute_position; - float vigor_ratio = 1; - float vigor = 0; - int age = 0; - float philotaxis_angle = 0; - bool is_lateral = false; // Track if this branch originated from a lateral bud - - BioNodeInfo(NodeType type, int age = 0, float philotaxis_angle = 0, bool is_lateral = false) - { - this->type = type; - this->age = age; - this->philotaxis_angle = philotaxis_angle; - this->is_lateral = is_lateral; - }; -}; - } // namespace Mtree diff --git a/m_tree/source/tree_functions/base_types/Property.hpp b/m_tree/source/tree_functions/base_types/Property.hpp index 9a85af9..4b0c7e0 100644 --- a/m_tree/source/tree_functions/base_types/Property.hpp +++ b/m_tree/source/tree_functions/base_types/Property.hpp @@ -1,11 +1,17 @@ #pragma once #include "source/utilities/GeometryUtilities.hpp" #include "source/utilities/RandomGenerator.hpp" +#include #include namespace Mtree { +template +concept PropertyFunction = requires(T& prop, float x) { + { prop.execute(x) } -> std::convertible_to; +}; + struct Property { virtual float execute(float x) = 0; @@ -63,19 +69,19 @@ struct PropertyWrapper PropertyWrapper() { property = std::make_shared(1); }; - template PropertyWrapper(T& property) + template PropertyWrapper(T& prop) { - this->property = std::make_shared(property); + this->property = std::make_shared(prop); }; - template PropertyWrapper(T&& property) + template PropertyWrapper(T&& prop) { - this->property = std::make_shared(property); + this->property = std::make_shared(prop); }; - template void set_property(T& property) + template void set_property(T& prop) { - this->property = std::make_shared(property); + this->property = std::make_shared(prop); } float execute(float x) { return property->execute(x); }; diff --git a/m_tree/source/tree_functions/base_types/TreeFunction.hpp b/m_tree/source/tree_functions/base_types/TreeFunction.hpp index 4e98410..020d5ac 100644 --- a/m_tree/source/tree_functions/base_types/TreeFunction.hpp +++ b/m_tree/source/tree_functions/base_types/TreeFunction.hpp @@ -1,10 +1,17 @@ #pragma once #include "source/tree/Node.hpp" #include "source/utilities/RandomGenerator.hpp" +#include #include namespace Mtree { + +template +concept TreeFunctionType = requires(T& func, std::vector& stems, int id, int parent_id) { + { func.execute(stems, id, parent_id) } -> std::same_as; +}; + class TreeFunction { protected: diff --git a/m_tree/source/utilities/GeometryUtilities.cpp b/m_tree/source/utilities/GeometryUtilities.cpp index 2e89bf9..efd9213 100644 --- a/m_tree/source/utilities/GeometryUtilities.cpp +++ b/m_tree/source/utilities/GeometryUtilities.cpp @@ -1,5 +1,3 @@ -#define _USE_MATH_DEFINES - #include "GeometryUtilities.hpp" #include #include @@ -19,7 +17,7 @@ void add_circle(std::vector& points, Vector3 position, Vector3 directio for (size_t i = 0; i < n_points; i++) { - float circle_angle = M_PI * (float)i / n_points * 2; + float circle_angle = std::numbers::pi_v * (float)i / n_points * 2; Vector3 position_in_circle = Vector3{std::cos(circle_angle), std::sin(circle_angle), 0} * radius; position_in_circle = position + rot * position_in_circle; @@ -70,7 +68,7 @@ float lerp(float a, float b, float t) Vector3 get_orthogonal_vector(const Vector3& v) { Vector3 tmp; - if (abs(v.z()) < .95) + if (std::abs(v.z()) < .95) { tmp = Vector3{1, 0, 0}; } diff --git a/m_tree/source/utilities/GeometryUtilities.hpp b/m_tree/source/utilities/GeometryUtilities.hpp index 6c50c35..a16869f 100644 --- a/m_tree/source/utilities/GeometryUtilities.hpp +++ b/m_tree/source/utilities/GeometryUtilities.hpp @@ -2,12 +2,9 @@ #include #include #include +#include #include -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif - namespace Mtree { namespace Geometry diff --git a/m_tree/tests/CMakeLists.txt b/m_tree/tests/CMakeLists.txt index 044ff7c..c05f5b5 100644 --- a/m_tree/tests/CMakeLists.txt +++ b/m_tree/tests/CMakeLists.txt @@ -1,6 +1,6 @@ -cmake_minimum_required(VERSION 3.5...3.27) +cmake_minimum_required(VERSION 3.15...3.31) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/m_tree/tests/main.cpp b/m_tree/tests/main.cpp index de3974f..b30bfc2 100644 --- a/m_tree/tests/main.cpp +++ b/m_tree/tests/main.cpp @@ -13,18 +13,39 @@ using namespace Mtree; int main() { - std::cout<<"hello world"<(); - auto branch = std::make_shared(); - trunk->add_child(branch); - branch->start_radius = ConstantProperty{ 1.5 }; - //trunk->length = 0.001; - Tree tree(trunk); - tree.execute_functions(); - ManifoldMesher mesher; - mesher.radial_resolution = 32; - mesher.mesh_tree(tree); + std::cout << "Testing BranchFunction (BranchGrowthInfo variant)..." << std::endl; + // Test 1: BranchFunction uses BranchGrowthInfo + { + auto trunk = std::make_shared(); + auto branch = std::make_shared(); + trunk->add_child(branch); + branch->start_radius = ConstantProperty{1.5}; + Tree tree(trunk); + tree.execute_functions(); + ManifoldMesher mesher; + mesher.radial_resolution = 32; + auto mesh = mesher.mesh_tree(tree); + std::cout << " Vertices: " << mesh.vertices.size() << std::endl; + } + + std::cout << "Testing GrowthFunction (BioNodeInfo variant)..." << std::endl; + + // Test 2: GrowthFunction uses BioNodeInfo + { + auto trunk = std::make_shared(); + auto growth = std::make_shared(); + growth->iterations = 3; + growth->enable_lateral_branching = true; + trunk->add_child(growth); + Tree tree(trunk); + tree.execute_functions(); + ManifoldMesher mesher; + mesher.radial_resolution = 16; + auto mesh = mesher.mesh_tree(tree); + std::cout << " Vertices: " << mesh.vertices.size() << std::endl; + } + + std::cout << "All tests passed!" << std::endl; return 0; } diff --git a/pyproject.toml b/pyproject.toml index cd352f1..031776e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "modular-tree" -version = "5.3.0" +version = "5.3.1" requires-python = ">=3.11" [dependency-groups] diff --git a/python_classes/nodes/base_types/node_tree.py b/python_classes/nodes/base_types/node_tree.py index 5da9e19..da2861c 100644 --- a/python_classes/nodes/base_types/node_tree.py +++ b/python_classes/nodes/base_types/node_tree.py @@ -5,3 +5,14 @@ class MtreeNodeTree(bpy.types.NodeTree): bl_idname = "mt_MtreeNodeTree" bl_label = "Mtree" bl_icon = "ONIONSKIN_ON" + + def update(self): + """Called when links or topology change.""" + # Import here to avoid circular imports + from ..tree_function_nodes.tree_mesher_node import debounced_build + + for node in self.nodes: + if node.bl_idname == "mt_MesherNode": + if getattr(node, "auto_update", True): + debounced_build(node) + break diff --git a/python_classes/nodes/sockets/bool_socket.py b/python_classes/nodes/sockets/bool_socket.py index b05e660..19a8ee4 100644 --- a/python_classes/nodes/sockets/bool_socket.py +++ b/python_classes/nodes/sockets/bool_socket.py @@ -1,6 +1,7 @@ import bpy from ..base_types.socket import MtreeSocket +from ..tree_function_nodes.tree_mesher_node import debounced_build class MtreeBoolSocket(bpy.types.NodeSocket, MtreeSocket): @@ -12,7 +13,10 @@ class MtreeBoolSocket(bpy.types.NodeSocket, MtreeSocket): def update_value(self, context): mesher = self.node.get_mesher() if mesher is not None: - mesher.build_tree() + if getattr(mesher, "auto_update", True): + debounced_build(mesher) + else: + mesher.build_tree() property_value: bpy.props.BoolProperty(default=True, update=update_value) diff --git a/python_classes/nodes/sockets/float_socket.py b/python_classes/nodes/sockets/float_socket.py index 3409094..3b11898 100644 --- a/python_classes/nodes/sockets/float_socket.py +++ b/python_classes/nodes/sockets/float_socket.py @@ -1,6 +1,7 @@ import bpy from ..base_types.socket import MtreeSocket +from ..tree_function_nodes.tree_mesher_node import debounced_build class MtreeFloatSocket(bpy.types.NodeSocket, MtreeSocket): @@ -16,7 +17,10 @@ def update_value(self, context): self["property_value"] = max(self.min_value, min(self.max_value, self.property_value)) mesher = self.node.get_mesher() if mesher is not None: - mesher.build_tree() + if getattr(mesher, "auto_update", True): + debounced_build(mesher) + else: + mesher.build_tree() property_value: bpy.props.FloatProperty(default=0, update=update_value) diff --git a/python_classes/nodes/sockets/int_socket.py b/python_classes/nodes/sockets/int_socket.py index 9097fd8..3b54e41 100644 --- a/python_classes/nodes/sockets/int_socket.py +++ b/python_classes/nodes/sockets/int_socket.py @@ -1,6 +1,7 @@ import bpy from ..base_types.socket import MtreeSocket +from ..tree_function_nodes.tree_mesher_node import debounced_build class MtreeIntSocket(bpy.types.NodeSocket, MtreeSocket): @@ -16,7 +17,10 @@ def update_value(self, context): self["property_value"] = max(self.min_value, min(self.max_value, self.property_value)) mesher = self.node.get_mesher() if mesher is not None: - mesher.build_tree() + if getattr(mesher, "auto_update", True): + debounced_build(mesher) + else: + mesher.build_tree() property_value: bpy.props.IntProperty(default=0, update=update_value) diff --git a/python_classes/nodes/sockets/property_socket.py b/python_classes/nodes/sockets/property_socket.py index 534daaa..49d3e54 100644 --- a/python_classes/nodes/sockets/property_socket.py +++ b/python_classes/nodes/sockets/property_socket.py @@ -2,6 +2,7 @@ from ...m_tree_wrapper import lazy_m_tree from ..base_types.socket import MtreeSocket +from ..tree_function_nodes.tree_mesher_node import debounced_build class MtreePropertySocket(bpy.types.NodeSocket, MtreeSocket): @@ -17,7 +18,10 @@ def update_value(self, context): self["property_value"] = max(self.min_value, min(self.max_value, self.property_value)) mesher = self.node.get_mesher() if mesher is not None: - mesher.build_tree() + if getattr(mesher, "auto_update", True): + debounced_build(mesher) + else: + mesher.build_tree() property_value: bpy.props.FloatProperty(default=0, update=update_value) diff --git a/python_classes/nodes/tree_function_nodes/branch_node.py b/python_classes/nodes/tree_function_nodes/branch_node.py index bee4ab5..d21c659 100644 --- a/python_classes/nodes/tree_function_nodes/branch_node.py +++ b/python_classes/nodes/tree_function_nodes/branch_node.py @@ -8,6 +8,7 @@ from ...viewport.shape_formulas import BLENDER_SHAPE_MAP from ...viewport.shape_formulas import CrownShape as PyCrownShape from ..base_types.node import MtreeFunctionNode +from .tree_mesher_node import debounced_build # Parameter groupings for organized UI BASIC_PARAMS = ["seed", "start", "end", "length", "branches_density", "start_angle"] @@ -38,6 +39,13 @@ } +def _update_crown_property(self, context): + """Trigger auto-update when crown shape properties change.""" + mesher = self.get_mesher() + if mesher is not None and getattr(mesher, "auto_update", True): + debounced_build(mesher) + + class BranchNode(bpy.types.Node, MtreeFunctionNode): bl_idname = "mt_BranchNode" bl_label = "Branches" @@ -64,6 +72,7 @@ class BranchNode(bpy.types.Node, MtreeFunctionNode): ], default="CYLINDRICAL", description="Crown shape envelope that controls branch length based on height", + update=_update_crown_property, ) angle_variation: bpy.props.FloatProperty( @@ -72,6 +81,7 @@ class BranchNode(bpy.types.Node, MtreeFunctionNode): min=-45.0, max=45.0, description="Height-based angle variation: positive = upward at top, downward at base", + update=_update_crown_property, ) show_crown_preview: bpy.props.BoolProperty( diff --git a/python_classes/nodes/tree_function_nodes/tree_mesher_node.py b/python_classes/nodes/tree_function_nodes/tree_mesher_node.py index 3bed99b..d31ac8e 100644 --- a/python_classes/nodes/tree_function_nodes/tree_mesher_node.py +++ b/python_classes/nodes/tree_function_nodes/tree_mesher_node.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import time import bpy @@ -10,9 +11,35 @@ from ...mesh_utils import create_mesh_from_cpp from ..base_types.node import MtreeNode +# Debounce state for auto-update +_pending_timers = {} +_DEBOUNCE_DELAY = 0.3 # seconds + + +def debounced_build(mesher, delay=_DEBOUNCE_DELAY): + """Schedule a debounced tree rebuild.""" + mesher_id = id(mesher) + + # Cancel existing timer for this mesher + if mesher_id in _pending_timers: + with contextlib.suppress(ValueError): + bpy.app.timers.unregister(_pending_timers[mesher_id]) + + def do_build(): + if mesher_id in _pending_timers: + del _pending_timers[mesher_id] + mesher.build_tree() + return None # Don't repeat + + _pending_timers[mesher_id] = do_build + bpy.app.timers.register(do_build, first_interval=delay) + def on_update_prop(node, context): - node.build_tree() + if getattr(node, "auto_update", True): + debounced_build(node) + else: + node.build_tree() class TreeMesherNode(bpy.types.Node, MtreeNode): @@ -21,6 +48,11 @@ class TreeMesherNode(bpy.types.Node, MtreeNode): bl_idname = "mt_MesherNode" bl_label = "Tree Mesher" + auto_update: bpy.props.BoolProperty( + name="Auto Update", + description="Automatically rebuild tree when parameters or connections change", + default=True, + ) radial_resolution: bpy.props.IntProperty( name="Radial Resolution", default=32, min=3, update=on_update_prop ) @@ -70,6 +102,7 @@ def _draw_tree_object_selector(self, container, context): ) def _draw_properties(self, container): + container.prop(self, "auto_update") container.prop(self, "radial_resolution") container.prop(self, "smoothness") diff --git a/uv.lock b/uv.lock index 04acb04..214a424 100644 --- a/uv.lock +++ b/uv.lock @@ -31,7 +31,7 @@ wheels = [ [[package]] name = "modular-tree" -version = "5.3.0" +version = "5.3.1" source = { virtual = "." } [package.dev-dependencies]