diff options
author | dilithjay <dilithjay@gmail.com> | 2021-09-22 17:41:12 +0300 |
---|---|---|
committer | dilithjay <dilithjay@gmail.com> | 2021-09-22 17:41:12 +0300 |
commit | 0d350e0193f1af82274ba78f5f93702c14646a86 (patch) | |
tree | 677b861ca8dd2fb7a34deb58644c6acff2756c07 /source/blender | |
parent | 204b01a254ac2445fea217e5211b2ed6aef631ca (diff) |
Geometry Nodes: Curve Fillet Node
This node can be used to fillet splines at control points to
create a circular arc.
The implementation roughly follows T89227's design.
The node works in two main modes: Bezier and Poly
* Bezier: Creates a circular arc at vertices by changing
handle lengths (applicable only for Bezier splines).
* Poly: Creates a circular arc by creating vertices (as
many as defined by the Count fields input) along
the arc (applicable for all spline types).
In both modes, the radius of the created arc is defined
by the Radius fields input.
The Limit Radius attribute can be enabled to prevent
overlapping when the defined radius exceeds the maximum
possible radius for a given point.
Reviewed By: Hans Goudey
Differential Revision: https://developer.blender.org/D12115
Diffstat (limited to 'source/blender')
-rw-r--r-- | source/blender/blenkernel/BKE_node.h | 1 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/node.cc | 1 | ||||
-rw-r--r-- | source/blender/makesdna/DNA_node_types.h | 10 | ||||
-rw-r--r-- | source/blender/makesrna/intern/rna_nodetree.c | 26 | ||||
-rw-r--r-- | source/blender/nodes/CMakeLists.txt | 1 | ||||
-rw-r--r-- | source/blender/nodes/NOD_geometry.h | 1 | ||||
-rw-r--r-- | source/blender/nodes/NOD_static_types.h | 1 | ||||
-rw-r--r-- | source/blender/nodes/geometry/nodes/node_geo_curve_fillet.cc | 629 |
8 files changed, 670 insertions, 0 deletions
diff --git a/source/blender/blenkernel/BKE_node.h b/source/blender/blenkernel/BKE_node.h index 42e2cda8de3..21ca65baf00 100644 --- a/source/blender/blenkernel/BKE_node.h +++ b/source/blender/blenkernel/BKE_node.h @@ -1499,6 +1499,7 @@ int ntreeTexExecTree(struct bNodeTree *ntree, #define GEO_NODE_INPUT_TANGENT 1086 #define GEO_NODE_STRING_JOIN 1087 #define GEO_NODE_CURVE_PARAMETER 1088 +#define GEO_NODE_CURVE_FILLET 1089 /** \} */ diff --git a/source/blender/blenkernel/intern/node.cc b/source/blender/blenkernel/intern/node.cc index 2d0239740f8..3c54d88c93a 100644 --- a/source/blender/blenkernel/intern/node.cc +++ b/source/blender/blenkernel/intern/node.cc @@ -5200,6 +5200,7 @@ static void registerGeometryNodes() register_node_type_geo_curve_set_handles(); register_node_type_geo_curve_spline_type(); register_node_type_geo_curve_subdivide(); + register_node_type_geo_curve_fillet(); register_node_type_geo_curve_to_mesh(); register_node_type_geo_curve_to_points(); register_node_type_geo_curve_trim(); diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index cf159a1e28d..18545666796 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -1447,6 +1447,11 @@ typedef struct NodeGeometryCurveSubdivide { uint8_t cuts_type; } NodeGeometryCurveSubdivide; +typedef struct NodeGeometryCurveFillet { + /* GeometryNodeCurveFilletMode. */ + uint8_t mode; +} NodeGeometryCurveFillet; + typedef struct NodeGeometryCurveTrim { /* GeometryNodeCurveSampleMode. */ uint8_t mode; @@ -2060,6 +2065,11 @@ typedef enum GeometryNodeCurveSampleMode { GEO_NODE_CURVE_SAMPLE_LENGTH = 1, } GeometryNodeCurveSampleMode; +typedef enum GeometryNodeCurveFilletMode { + GEO_NODE_CURVE_FILLET_BEZIER = 0, + GEO_NODE_CURVE_FILLET_POLY = 1, +} GeometryNodeCurveFilletMode; + typedef enum GeometryNodeAttributeTransferMapMode { GEO_NODE_ATTRIBUTE_TRANSFER_NEAREST_FACE_INTERPOLATED = 0, GEO_NODE_ATTRIBUTE_TRANSFER_NEAREST = 1, diff --git a/source/blender/makesrna/intern/rna_nodetree.c b/source/blender/makesrna/intern/rna_nodetree.c index ec53f35df4c..b631e76c094 100644 --- a/source/blender/makesrna/intern/rna_nodetree.c +++ b/source/blender/makesrna/intern/rna_nodetree.c @@ -10193,6 +10193,32 @@ static void def_geo_curve_subdivide(StructRNA *srna) RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update"); } +static void def_geo_curve_fillet(StructRNA *srna) +{ + PropertyRNA *prop; + + static EnumPropertyItem mode_items[] = { + {GEO_NODE_CURVE_FILLET_BEZIER, + "BEZIER", + 0, + "Bezier", + "Align Bezier handles to create circular arcs at each control point"}, + {GEO_NODE_CURVE_FILLET_POLY, + "POLY", + 0, + "Poly", + "Add control points along a circular arc (handle type is vector if Bezier Spline)"}, + {0, NULL, 0, NULL, NULL}, + }; + + RNA_def_struct_sdna_from(srna, "NodeGeometryCurveFillet", "storage"); + + prop = RNA_def_property(srna, "mode", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_items(prop, mode_items); + RNA_def_property_ui_text(prop, "Mode", "How to choose number of vertices on fillet"); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update"); +} + static void def_geo_curve_to_points(StructRNA *srna) { PropertyRNA *prop; diff --git a/source/blender/nodes/CMakeLists.txt b/source/blender/nodes/CMakeLists.txt index a8795649ede..e6af3ecafbc 100644 --- a/source/blender/nodes/CMakeLists.txt +++ b/source/blender/nodes/CMakeLists.txt @@ -191,6 +191,7 @@ set(SRC geometry/nodes/node_geo_curve_set_handles.cc geometry/nodes/node_geo_curve_spline_type.cc geometry/nodes/node_geo_curve_subdivide.cc + geometry/nodes/node_geo_curve_fillet.cc geometry/nodes/node_geo_curve_to_mesh.cc geometry/nodes/node_geo_curve_to_points.cc geometry/nodes/node_geo_curve_trim.cc diff --git a/source/blender/nodes/NOD_geometry.h b/source/blender/nodes/NOD_geometry.h index 24f60263d8a..47bc54132eb 100644 --- a/source/blender/nodes/NOD_geometry.h +++ b/source/blender/nodes/NOD_geometry.h @@ -73,6 +73,7 @@ void register_node_type_geo_curve_reverse(void); void register_node_type_geo_curve_set_handles(void); void register_node_type_geo_curve_spline_type(void); void register_node_type_geo_curve_subdivide(void); +void register_node_type_geo_curve_fillet(void); void register_node_type_geo_curve_to_mesh(void); void register_node_type_geo_curve_to_points(void); void register_node_type_geo_curve_trim(void); diff --git a/source/blender/nodes/NOD_static_types.h b/source/blender/nodes/NOD_static_types.h index 8fb18e839a7..ab673d814bb 100644 --- a/source/blender/nodes/NOD_static_types.h +++ b/source/blender/nodes/NOD_static_types.h @@ -328,6 +328,7 @@ DefNode(GeometryNode, GEO_NODE_CURVE_PRIMITIVE_QUADRILATERAL, def_geo_curve_prim DefNode(GeometryNode, GEO_NODE_CURVE_PRIMITIVE_SPIRAL, 0, "CURVE_PRIMITIVE_SPIRAL", CurveSpiral, "Curve Spiral", "") DefNode(GeometryNode, GEO_NODE_CURVE_PRIMITIVE_STAR, 0, "CURVE_PRIMITIVE_STAR", CurveStar, "Star", "") DefNode(GeometryNode, GEO_NODE_CURVE_RESAMPLE, def_geo_curve_resample, "CURVE_RESAMPLE", CurveResample, "Resample Curve", "") +DefNode(GeometryNode, GEO_NODE_CURVE_FILLET, def_geo_curve_fillet, "CURVE_FILLET", CurveFillet, "Curve Fillet", "") DefNode(GeometryNode, GEO_NODE_CURVE_TO_MESH, 0, "CURVE_TO_MESH", CurveToMesh, "Curve to Mesh", "") DefNode(GeometryNode, GEO_NODE_CURVE_TO_POINTS, def_geo_curve_to_points, "CURVE_TO_POINTS", CurveToPoints, "Curve to Points", "") DefNode(GeometryNode, GEO_NODE_CURVE_TRIM, def_geo_curve_trim, "CURVE_TRIM", CurveTrim, "Curve Trim", "") diff --git a/source/blender/nodes/geometry/nodes/node_geo_curve_fillet.cc b/source/blender/nodes/geometry/nodes/node_geo_curve_fillet.cc new file mode 100644 index 00000000000..830cfcc8331 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_curve_fillet.cc @@ -0,0 +1,629 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "BLI_task.hh" + +#include "UI_interface.h" +#include "UI_resources.h" + +#include "DNA_node_types.h" + +#include "node_geometry_util.hh" + +#include "BKE_spline.hh" + +namespace blender::nodes { + +static void geo_node_curve_fillet_declare(NodeDeclarationBuilder &b) +{ + b.add_input<decl::Geometry>("Curve"); + b.add_input<decl::Int>("Count").default_value(1).min(1).max(1000); + b.add_input<decl::Float>("Radius") + .min(0.0f) + .max(FLT_MAX) + .subtype(PropertySubType::PROP_DISTANCE) + .default_value(0.25f); + b.add_input<decl::Bool>("Limit Radius"); + b.add_output<decl::Geometry>("Curve"); +} + +static void geo_node_curve_fillet_layout(uiLayout *layout, bContext *UNUSED(C), PointerRNA *ptr) +{ + uiItemR(layout, ptr, "mode", UI_ITEM_R_EXPAND, nullptr, ICON_NONE); +} + +static void geo_node_curve_fillet_init(bNodeTree *UNUSED(tree), bNode *node) +{ + NodeGeometryCurveFillet *data = (NodeGeometryCurveFillet *)MEM_callocN( + sizeof(NodeGeometryCurveFillet), __func__); + + data->mode = GEO_NODE_CURVE_FILLET_BEZIER; + node->storage = data; +} + +struct FilletParam { + GeometryNodeCurveFilletMode mode; + + /* Number of points to be added. */ + const VArray<int> *counts; + + /* Radii for fillet arc at all vertices. */ + const VArray<float> *radii; + + /* Whether or not fillets are allowed to overlap. */ + bool limit_radius; +}; + +/* A data structure used to store fillet data about all vertices to be filleted. */ +struct FilletData { + Span<float3> positions; + Array<float3> directions, axes; + Array<float> radii, angles; + Array<int> counts; +}; + +static void geo_node_curve_fillet_update(bNodeTree *UNUSED(ntree), bNode *node) +{ + NodeGeometryCurveFillet &node_storage = *(NodeGeometryCurveFillet *)node->storage; + const GeometryNodeCurveFilletMode mode = (GeometryNodeCurveFilletMode)node_storage.mode; + + bNodeSocket *poly_socket = ((bNodeSocket *)node->inputs.first)->next; + + nodeSetSocketAvailability(poly_socket, mode == GEO_NODE_CURVE_FILLET_POLY); +} + +/* Function to get the center of a fillet. */ +static float3 get_center(const float3 vec_pos2prev, + const float3 pos, + const float3 axis, + const float angle) +{ + float3 vec_pos2center; + rotate_normalized_v3_v3v3fl(vec_pos2center, vec_pos2prev, axis, M_PI_2 - angle / 2.0f); + vec_pos2center *= 1.0f / sinf(angle / 2.0f); + + return vec_pos2center + pos; +} + +/* Function to get the center of the fillet using fillet data */ +static float3 get_center(const float3 vec_pos2prev, const FilletData &fd, const int index) +{ + const float angle = fd.angles[index]; + const float3 axis = fd.axes[index]; + const float3 pos = fd.positions[index]; + + return get_center(vec_pos2prev, pos, axis, angle); +} + +/* Calculate the direction vectors from each vertex to their previous vertex. */ +static Array<float3> calculate_directions(const Span<float3> positions) +{ + const int size = positions.size(); + Array<float3> directions(size); + + for (const int i : IndexRange(size - 1)) { + directions[i] = (positions[i + 1] - positions[i]).normalized(); + } + directions[size - 1] = (positions[0] - positions[size - 1]).normalized(); + + return directions; +} + +/* Calculate the axes around which the fillet is built. */ +static Array<float3> calculate_axes(const Span<float3> directions) +{ + const int size = directions.size(); + Array<float3> axes(size); + + axes[0] = float3::cross(-directions[size - 1], directions[0]).normalized(); + for (const int i : IndexRange(1, size - 1)) { + axes[i] = float3::cross(-directions[i - 1], directions[i]).normalized(); + } + + return axes; +} + +/* Calculate the angle of the arc formed by the fillet. */ +static Array<float> calculate_angles(const Span<float3> directions) +{ + const int size = directions.size(); + Array<float> angles(size); + + angles[0] = M_PI - angle_v3v3(-directions[size - 1], directions[0]); + for (const int i : IndexRange(1, size - 1)) { + angles[i] = M_PI - angle_v3v3(-directions[i - 1], directions[i]); + } + + return angles; +} + +/* Calculate the segment count in each filleted arc. */ +static Array<int> calculate_counts(const FilletParam &fillet_param, + const int size, + const int spline_offset, + const bool cyclic) +{ + Array<int> counts(size, 1); + if (fillet_param.mode == GEO_NODE_CURVE_FILLET_POLY) { + for (const int i : IndexRange(size)) { + counts[i] = (*fillet_param.counts)[spline_offset + i]; + } + } + if (!cyclic) { + counts[0] = counts[size - 1] = 0; + } + + return counts; +} + +/* Calculate the radii for the vertices to be filleted. */ +static Array<float> calculate_radii(const FilletParam &fillet_param, + const int size, + const int spline_offset) +{ + Array<float> radii(size, 0.0f); + if (fillet_param.limit_radius) { + for (const int i : IndexRange(size)) { + radii[i] = std::max((*fillet_param.radii)[spline_offset + i], 0.0f); + } + } + else { + for (const int i : IndexRange(size)) { + radii[i] = (*fillet_param.radii)[spline_offset + i]; + } + } + + return radii; +} + +/* Calculate the number of vertices added per vertex on the source spline. */ +static int calculate_point_counts(MutableSpan<int> point_counts, + const Span<float> radii, + const Span<int> counts) +{ + int added_count = 0; + for (const int i : IndexRange(point_counts.size())) { + /* Calculate number of points to be added for the vertex. */ + if (radii[i] != 0.0f) { + added_count += counts[i]; + point_counts[i] = counts[i] + 1; + } + } + + return added_count; +} + +static FilletData calculate_fillet_data(const Spline &spline, + const FilletParam &fillet_param, + int &added_count, + MutableSpan<int> point_counts, + const int spline_offset) +{ + const int size = spline.size(); + + FilletData fd; + fd.directions = calculate_directions(spline.positions()); + fd.positions = spline.positions(); + fd.axes = calculate_axes(fd.directions); + fd.angles = calculate_angles(fd.directions); + fd.counts = calculate_counts(fillet_param, size, spline_offset, spline.is_cyclic()); + fd.radii = calculate_radii(fillet_param, size, spline_offset); + + added_count = calculate_point_counts(point_counts, fd.radii, fd.counts); + + return fd; +} + +/* Limit the radius based on angle and radii to prevent overlapping. */ +static void limit_radii(FilletData &fd, const bool cyclic) +{ + MutableSpan<float> radii(fd.radii); + Span<float> angles(fd.angles); + Span<float3> positions(fd.positions); + + const int size = radii.size(); + const int fillet_count = cyclic ? size : size - 2; + const int start = cyclic ? 0 : 1; + Array<float> max_radii(size, FLT_MAX); + + if (cyclic) { + /* Calculate lengths between adjacent control points. */ + const float len_prev = float3::distance(positions[0], positions[size - 1]); + const float len_next = float3::distance(positions[0], positions[1]); + + /* Calculate tangent lengths of fillets in control points. */ + const float tan_len = radii[0] * tan(angles[0] / 2.0f); + const float tan_len_prev = radii[size - 1] * tan(angles[size - 1] / 2.0f); + const float tan_len_next = radii[1] * tan(angles[1] / 2.0f); + + float factor_prev = 1.0f, factor_next = 1.0f; + if (tan_len + tan_len_prev > len_prev) { + factor_prev = len_prev / (tan_len + tan_len_prev); + } + if (tan_len + tan_len_next > len_next) { + factor_next = len_next / (tan_len + tan_len_next); + } + + /* Scale max radii by calculated factors. */ + max_radii[0] = radii[0] * std::min(factor_next, factor_prev); + max_radii[1] = radii[1] * factor_next; + max_radii[size - 1] = radii[size - 1] * factor_prev; + } + + /* Initialize max_radii to largest possible radii. */ + float prev_dist = float3::distance(positions[1], positions[0]); + for (const int i : IndexRange(1, size - 2)) { + const float temp_dist = float3::distance(positions[i], positions[i + 1]); + max_radii[i] = std::min(prev_dist, temp_dist) / tan(angles[i] / 2.0f); + prev_dist = temp_dist; + } + + /* Max radii calculations for each index. */ + for (const int i : IndexRange(start, fillet_count - 1)) { + const float len_next = float3::distance(positions[i], positions[i + 1]); + const float tan_len = radii[i] * tan(angles[i] / 2.0f); + const float tan_len_next = radii[i + 1] * tan(angles[i + 1] / 2.0f); + + /* Scale down radii if too large for segment. */ + float factor = 1.0f; + if (tan_len + tan_len_next > len_next) { + factor = len_next / (tan_len + tan_len_next); + } + max_radii[i] = std::min(max_radii[i], radii[i] * factor); + max_radii[i + 1] = std::min(max_radii[i + 1], radii[i + 1] * factor); + } + + /* Assign the max_radii to the fillet data's radii. */ + for (const int i : IndexRange(size)) { + radii[i] = max_radii[i]; + } +} + +/* + * Create a mapping from each vertex in the destination spline to that of the source spline. + * Used for copying the data from the source spline. + */ +static Array<int> create_dst_to_src_map(const Span<int> point_counts, const int total_points) +{ + Array<int> map(total_points); + MutableSpan<int> map_span{map}; + int index = 0; + + for (const int i : point_counts.index_range()) { + map_span.slice(index, point_counts[i]).fill(i); + index += point_counts[i]; + } + + BLI_assert(index == total_points); + + return map; +} + +template<typename T> +static void copy_attribute_by_mapping(const Span<T> src, + MutableSpan<T> dst, + const Span<int> mapping) +{ + for (const int i : dst.index_range()) { + dst[i] = src[mapping[i]]; + } +} + +/* Copy radii and tilts from source spline to destination. Positions are handled later in update + * positions methods. */ +static void copy_common_attributes_by_mapping(const Spline &src, + Spline &dst, + const Span<int> mapping) +{ + copy_attribute_by_mapping(src.radii(), dst.radii(), mapping); + copy_attribute_by_mapping(src.tilts(), dst.tilts(), mapping); + + dst.attributes.reallocate(1); + src.attributes.foreach_attribute( + [&](const AttributeIDRef &attribute_id, const AttributeMetaData &meta_data) { + std::optional<GSpan> src_attribute = src.attributes.get_for_read(attribute_id); + if (dst.attributes.create(attribute_id, meta_data.data_type)) { + std::optional<GMutableSpan> dst_attribute = dst.attributes.get_for_write(attribute_id); + if (dst_attribute) { + src_attribute->type().copy_assign(src_attribute->data(), dst_attribute->data()); + return true; + } + } + BLI_assert_unreachable(); + return false; + }, + ATTR_DOMAIN_POINT); +} + +/* Update the vertex positions and handle positions of a Bezier spline based on fillet data. */ +static void update_bezier_positions(const FilletData &fd, + BezierSpline &dst_spline, + const BezierSpline &src_spline, + const Span<int> point_counts) +{ + Span<float> radii(fd.radii); + Span<float> angles(fd.angles); + Span<float3> axes(fd.axes); + Span<float3> positions(fd.positions); + Span<float3> directions(fd.directions); + + const int size = radii.size(); + + int i_dst = 0; + for (const int i_src : IndexRange(size)) { + const int count = point_counts[i_src]; + + /* Skip if the point count for the vertex is 1. */ + if (count == 1) { + dst_spline.positions()[i_dst] = src_spline.positions()[i_src]; + dst_spline.handle_types_left()[i_dst] = src_spline.handle_types_left()[i_src]; + dst_spline.handle_types_right()[i_dst] = src_spline.handle_types_right()[i_src]; + dst_spline.handle_positions_left()[i_dst] = src_spline.handle_positions_left()[i_src]; + dst_spline.handle_positions_right()[i_dst] = src_spline.handle_positions_right()[i_src]; + i_dst++; + continue; + } + + /* Calculate the angle to be formed between any 2 adjacent vertices within the fillet. */ + const float segment_angle = angles[i_src] / (count - 1); + /* Calculate the handle length for each added vertex. Equation: L = 4R/3 * tan(A/4) */ + const float handle_length = 4.0f * radii[i_src] / 3.0f * tan(segment_angle / 4.0f); + /* Calculate the distance by which each vertex should be displaced from their initial position. + */ + const float displacement = radii[i_src] * tan(angles[i_src] / 2.0f); + + /* Position the end points of the arc and their handles. */ + const int end_i = i_dst + count - 1; + const float3 prev_dir = i_src == 0 ? -directions[size - 1] : -directions[i_src - 1]; + const float3 next_dir = directions[i_src]; + dst_spline.positions()[i_dst] = positions[i_src] + displacement * prev_dir; + dst_spline.positions()[end_i] = positions[i_src] + displacement * next_dir; + dst_spline.handle_positions_right()[i_dst] = dst_spline.positions()[i_dst] - + handle_length * prev_dir; + dst_spline.handle_positions_left()[end_i] = dst_spline.positions()[end_i] - + handle_length * next_dir; + dst_spline.handle_types_right()[i_dst] = dst_spline.handle_types_left()[end_i] = + BezierSpline::HandleType::Align; + dst_spline.handle_types_left()[i_dst] = dst_spline.handle_types_right()[end_i] = + BezierSpline::HandleType::Vector; + dst_spline.mark_cache_invalid(); + + /* Calculate the center of the radius to be formed. */ + const float3 center = get_center(dst_spline.positions()[i_dst] - positions[i_src], fd, i_src); + /* Calculate the vector of the radius formed by the first vertex. */ + float3 radius_vec = dst_spline.positions()[i_dst] - center; + const float radius = radius_vec.normalize_and_get_length(); + + dst_spline.handle_types_right().slice(1, count - 2).fill(BezierSpline::HandleType::Align); + dst_spline.handle_types_left().slice(1, count - 2).fill(BezierSpline::HandleType::Align); + + /* For each of the vertices in between the end points. */ + for (const int j : IndexRange(1, count - 2)) { + int index = i_dst + j; + /* Rotate the radius by the segment angle and determine its tangent (used for getting handle + * directions). */ + float3 new_radius_vec, tangent_vec; + rotate_normalized_v3_v3v3fl(new_radius_vec, radius_vec, -axes[i_src], segment_angle); + rotate_normalized_v3_v3v3fl(tangent_vec, new_radius_vec, axes[i_src], M_PI_2); + radius_vec = new_radius_vec; + tangent_vec *= handle_length; + + /* Adjust the positions of the respective vertex and its handles. */ + dst_spline.positions()[index] = center + new_radius_vec * radius; + dst_spline.handle_positions_left()[index] = dst_spline.positions()[index] + tangent_vec; + dst_spline.handle_positions_right()[index] = dst_spline.positions()[index] - tangent_vec; + } + + i_dst += count; + } +} + +/* Update the vertex positions of a Poly spline based on fillet data. */ +static void update_poly_positions(const FilletData &fd, + Spline &dst_spline, + const Spline &src_spline, + const Span<int> point_counts) +{ + Span<float> radii(fd.radii); + Span<float> angles(fd.angles); + Span<float3> axes(fd.axes); + Span<float3> positions(fd.positions); + Span<float3> directions(fd.directions); + + const int size = radii.size(); + + int i_dst = 0; + for (const int i_src : IndexRange(size)) { + const int count = point_counts[i_src]; + + /* Skip if the point count for the vertex is 1. */ + if (count == 1) { + dst_spline.positions()[i_dst] = src_spline.positions()[i_src]; + i_dst++; + continue; + } + + const float segment_angle = angles[i_src] / (count - 1); + const float displacement = radii[i_src] * tan(angles[i_src] / 2.0f); + + /* Position the end points of the arc. */ + const int end_i = i_dst + count - 1; + const float3 prev_dir = i_src == 0 ? -directions[size - 1] : -directions[i_src - 1]; + const float3 next_dir = directions[i_src]; + dst_spline.positions()[i_dst] = positions[i_src] + displacement * prev_dir; + dst_spline.positions()[end_i] = positions[i_src] + displacement * next_dir; + + /* Calculate the center of the radius to be formed. */ + const float3 center = get_center(dst_spline.positions()[i_dst] - positions[i_src], fd, i_src); + /* Calculate the vector of the radius formed by the first vertex. */ + float3 radius_vec = dst_spline.positions()[i_dst] - center; + + for (const int j : IndexRange(1, count - 2)) { + /* Rotate the radius by the segment angle */ + float3 new_radius_vec; + rotate_normalized_v3_v3v3fl(new_radius_vec, radius_vec, -axes[i_src], segment_angle); + radius_vec = new_radius_vec; + + dst_spline.positions()[i_dst + j] = center + new_radius_vec; + } + + i_dst += count; + } +} + +static SplinePtr fillet_spline(const Spline &spline, + const FilletParam &fillet_param, + const int spline_offset) +{ + const int size = spline.size(); + const bool cyclic = spline.is_cyclic(); + + if (size < 3) { + return spline.copy(); + } + + /* Initialize the point_counts with 1s (at least one vertex on dst for each vertex on src). */ + Array<int> point_counts(size, 1); + + int added_count = 0; + /* Update point_counts array and added_count. */ + FilletData fd = calculate_fillet_data( + spline, fillet_param, added_count, point_counts, spline_offset); + if (fillet_param.limit_radius) { + limit_radii(fd, cyclic); + } + + const int total_points = added_count + size; + const Array<int> dst_to_src = create_dst_to_src_map(point_counts, total_points); + SplinePtr dst_spline_ptr = spline.copy_only_settings(); + (*dst_spline_ptr).resize(total_points); + copy_common_attributes_by_mapping(spline, *dst_spline_ptr, dst_to_src); + + switch (spline.type()) { + case Spline::Type::Bezier: { + const BezierSpline &src_spline = static_cast<const BezierSpline &>(spline); + BezierSpline &dst_spline = static_cast<BezierSpline &>(*dst_spline_ptr); + if (fillet_param.mode == GEO_NODE_CURVE_FILLET_POLY) { + dst_spline.handle_types_left().fill(BezierSpline::HandleType::Vector); + dst_spline.handle_types_right().fill(BezierSpline::HandleType::Vector); + update_poly_positions(fd, dst_spline, src_spline, point_counts); + } + else { + update_bezier_positions(fd, dst_spline, src_spline, point_counts); + } + break; + } + case Spline::Type::Poly: { + update_poly_positions(fd, *dst_spline_ptr, spline, point_counts); + break; + } + case Spline::Type::NURBS: { + const NURBSpline &src_spline = static_cast<const NURBSpline &>(spline); + NURBSpline &dst_spline = static_cast<NURBSpline &>(*dst_spline_ptr); + copy_attribute_by_mapping(src_spline.weights(), dst_spline.weights(), dst_to_src); + update_poly_positions(fd, dst_spline, src_spline, point_counts); + break; + } + } + + return dst_spline_ptr; +} + +static std::unique_ptr<CurveEval> fillet_curve(const CurveEval &input_curve, + const FilletParam &fillet_param) +{ + Span<SplinePtr> input_splines = input_curve.splines(); + + std::unique_ptr<CurveEval> output_curve = std::make_unique<CurveEval>(); + const int num_splines = input_splines.size(); + output_curve->resize(num_splines); + MutableSpan<SplinePtr> output_splines = output_curve->splines(); + Array<int> spline_offsets = input_curve.control_point_offsets(); + + threading::parallel_for(input_splines.index_range(), 128, [&](IndexRange range) { + for (const int i : range) { + output_splines[i] = fillet_spline(*input_splines[i], fillet_param, spline_offsets[i]); + } + }); + output_curve->attributes = input_curve.attributes; + + return output_curve; +} + +static void geo_node_fillet_exec(GeoNodeExecParams params) +{ + GeometrySet geometry_set = params.extract_input<GeometrySet>("Curve"); + + geometry_set = bke::geometry_set_realize_instances(geometry_set); + + if (!geometry_set.has_curve()) { + params.set_output("Curve", geometry_set); + return; + } + + NodeGeometryCurveFillet &node_storage = *(NodeGeometryCurveFillet *)params.node().storage; + const GeometryNodeCurveFilletMode mode = (GeometryNodeCurveFilletMode)node_storage.mode; + FilletParam fillet_param; + fillet_param.mode = mode; + + CurveComponent &component = geometry_set.get_component_for_write<CurveComponent>(); + GeometryComponentFieldContext field_context{component, ATTR_DOMAIN_POINT}; + const int domain_size = component.attribute_domain_size(ATTR_DOMAIN_POINT); + fn::FieldEvaluator field_evaluator{field_context, domain_size}; + + Field<float> radius_field = params.extract_input<Field<float>>("Radius"); + field_evaluator.add(std::move(radius_field)); + + if (mode == GEO_NODE_CURVE_FILLET_POLY) { + Field<int> count_field = params.extract_input<Field<int>>("Count"); + field_evaluator.add(std::move(count_field)); + } + + field_evaluator.evaluate(); + + fillet_param.radii = &field_evaluator.get_evaluated<float>(0); + if (fillet_param.radii->is_single() && fillet_param.radii->get_internal_single() < 0.0f) { + params.set_output("Geometry", geometry_set); + return; + } + + if (mode == GEO_NODE_CURVE_FILLET_POLY) { + fillet_param.counts = &field_evaluator.get_evaluated<int>(1); + } + + fillet_param.limit_radius = params.extract_input<bool>("Limit Radius"); + + const CurveEval &input_curve = *geometry_set.get_curve_for_read(); + std::unique_ptr<CurveEval> output_curve = fillet_curve(input_curve, fillet_param); + + params.set_output("Curve", GeometrySet::create_with_curve(output_curve.release())); +} +} // namespace blender::nodes + +void register_node_type_geo_curve_fillet() +{ + static bNodeType ntype; + + geo_node_type_base(&ntype, GEO_NODE_CURVE_FILLET, "Curve Fillet", NODE_CLASS_GEOMETRY, 0); + ntype.draw_buttons = blender::nodes::geo_node_curve_fillet_layout; + node_type_storage( + &ntype, "NodeGeometryCurveFillet", node_free_standard_storage, node_copy_standard_storage); + ntype.declare = blender::nodes::geo_node_curve_fillet_declare; + node_type_init(&ntype, blender::nodes::geo_node_curve_fillet_init); + node_type_update(&ntype, blender::nodes::geo_node_curve_fillet_update); + ntype.geometry_node_execute = blender::nodes::geo_node_fillet_exec; + nodeRegisterType(&ntype); +} |