diff --git a/core/socket_conversions.py b/core/socket_conversions.py index cc07c0d7bb..79eb04b9ba 100644 --- a/core/socket_conversions.py +++ b/core/socket_conversions.py @@ -19,7 +19,7 @@ from enum import Enum from sverchok.core.sv_custom_exceptions import ImplicitConversionProhibited -from sverchok.data_structure import get_data_nesting_level, is_ultimately +from sverchok.data_structure import get_data_nesting_level, is_ultimately, NUMERIC_DATA_TYPES from sverchok.utils.field.vector import SvVectorField, SvMatrixVectorField, SvConstantVectorField from sverchok.utils.field.scalar import SvScalarField, SvConstantScalarField from sverchok.utils.curve import SvCurve @@ -42,7 +42,7 @@ def matrices_to_vfield(data): def vertices_to_vfield(data): - if isinstance(data, (tuple, list)) and len(data) == 3 and isinstance(data[0], (float, int)): + if isinstance(data, (tuple, list)) and len(data) == 3 and isinstance(data[0], NUMERIC_DATA_TYPES): data = deepcopy(data) return SvConstantVectorField(data) elif isinstance(data, (list, tuple)): @@ -52,7 +52,7 @@ def vertices_to_vfield(data): def numbers_to_sfield(data): - if isinstance(data, (int, float)): + if isinstance(data, NUMERIC_DATA_TYPES): return SvConstantScalarField(data) elif isinstance(data, (list, tuple)): return [numbers_to_sfield(item) for item in data] @@ -68,7 +68,7 @@ def vectors_to_matrices(source_data): def get_all(data): for item in data: - if isinstance(item, (tuple, list, ndarray)) and len(item) == 3 and isinstance(item[0], (float, int)): + if isinstance(item, (tuple, list, ndarray)) and len(item) == 3 and isinstance(item[0], NUMERIC_DATA_TYPES): # generate location matrix from location x, y, z = item collect_matrix(Matrix([(1., .0, .0, x), (.0, 1., .0, y), (.0, .0, 1., z), (.0, .0, .0, 1.)])) @@ -97,7 +97,7 @@ def quaternions_to_matrices(source_data): def get_all(data): for item in data: - if isinstance(item, (tuple, list)) and len(item) == 4 and isinstance(item[0], (float, int)): + if isinstance(item, (tuple, list)) and len(item) == 4 and isinstance(item[0], NUMERIC_DATA_TYPES): mat = Quaternion(item).to_matrix().to_4x4() collect_matrix(mat) else: @@ -122,14 +122,14 @@ def get_all(data): def string_to_vector(source_data): # it can be so that socket is string but data their are already vectors, performance-wise we check only first item - if isinstance(source_data[0][0], (float, int)): + if isinstance(source_data[0][0], NUMERIC_DATA_TYPES): return [[(v, v, v) for v in obj] for obj in source_data] return source_data def string_to_color(source_data): # it can be so that socket is string but data their are already colors, performance-wise we check only first item - if isinstance(source_data[0][0], (float, int)): + if isinstance(source_data[0][0], NUMERIC_DATA_TYPES): return [[(v, v, v, 1) for v in obj] for obj in source_data] if len(source_data[0][0]) == 3: return vector_to_color(source_data) diff --git a/data_structure.py b/data_structure.py index 7492882ab8..9ba4295f70 100755 --- a/data_structure.py +++ b/data_structure.py @@ -503,6 +503,7 @@ def levels_of_list_or_np(lst): return level return 0 +NUMERIC_DATA_TYPES = (float, int, float64, int32, int64) SIMPLE_DATA_TYPES = (float, int, float64, int32, int64, str, Matrix) diff --git a/docs/assets/nodes/analyzer/inscribed_circle_1.png b/docs/assets/nodes/analyzer/inscribed_circle_1.png new file mode 100644 index 0000000000..a54c6f711d Binary files /dev/null and b/docs/assets/nodes/analyzer/inscribed_circle_1.png differ diff --git a/docs/assets/nodes/analyzer/inscribed_circle_2.png b/docs/assets/nodes/analyzer/inscribed_circle_2.png new file mode 100644 index 0000000000..0d28f66480 Binary files /dev/null and b/docs/assets/nodes/analyzer/inscribed_circle_2.png differ diff --git a/docs/nodes/analyzer/poly_inscribed_circle.rst b/docs/nodes/analyzer/poly_inscribed_circle.rst new file mode 100644 index 0000000000..77fa904159 --- /dev/null +++ b/docs/nodes/analyzer/poly_inscribed_circle.rst @@ -0,0 +1,68 @@ +Polygon Inscribed Circle +======================== + +Dependencies +------------ + +This node requires SciPy library to work. + +Functionality +------------- + +This node calculates the center and the radius of inscribed circle for each +convex face of the input mesh. Obviously, it is not always possible to inscribe +a circle into a polygon, if polygon is not a triangle. For non-tiangular +polygons, this node calculates the biggest circle which can be inscribed into +the polygon, i.e. the circle which touches as many polygon edges as possible. + +Inputs +------ + +This node has the following inputs: + +- **Vertices**. The vertices of the input mesh. This input is mandatory. +- **Faces**. The faces of the input mesh. This input is mandatory. + +Parameters +---------- + +This node has the following parameters: + +- **Flat Matrix output**. If checked, the node will generate a single flat list + of matrices in the **Matrix** output, for all input meshes. Checked by default. +- **On concave faces**. This parameter is available in the N panel only. + Defines what the node should do if it encounters a concave face. There are + the following options available: + + - **Skip**. Just skip such faces - do not generate inscribed circles for them. + - **Error**. Stop processing and give an error (turn the node red). + - **As Is**. Try to generate an inscribed circle for such face anyway. In + many cases, the generated circle will be incorrect (will be too small or + even outside the polygon), but in some simple cases it can be valid. + + The default option is **Skip**. + +Outputs +------- + +This node has the following outputs: + +- **Center**. For each inscribed circle, this contains a matrix, Z axis of + which points along face normal, and the translation component equals to the + center of the inscribed circle. This output can be used to actually place + circles at their places. +- **Radius**. Radiuses of the inscribed circles. + +Examples of Usage +----------------- + +In many cases inscribed circle can touch only two or three polygon edges: + +.. image:: ../../../docs/assets/nodes/analyzer/inscribed_circle_1.png + :target: ../../../docs/assets/nodes/analyzer/inscribed_circle_1.png + +If the polygon is almost regular, the circle will touch more edges: + +.. image:: ../../../docs/assets/nodes/analyzer/inscribed_circle_2.png + :target: ../../../docs/assets/nodes/analyzer/inscribed_circle_2.png + diff --git a/index.yaml b/index.yaml index 72fd9c98f6..f8022b23f6 100644 --- a/index.yaml +++ b/index.yaml @@ -392,6 +392,7 @@ - SvCircleApproxNode - SvSphereApproxNode - SvInscribedCircleNode + - SvSemiInscribedCircleNode - SvSteinerEllipseNode - --- - SvMeshSelectNodeMk2 diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index a2e6ff1701..230e10be85 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -219,6 +219,7 @@ - SvCircleApproxNode - SvSphereApproxNode - SvInscribedCircleNode + - SvSemiInscribedCircleNode - SvSteinerEllipseNode - --- - SvMeshSelectNodeMk2 diff --git a/menus/full_nortikin.yaml b/menus/full_nortikin.yaml index 01aa0bdfd9..4a6ca99296 100644 --- a/menus/full_nortikin.yaml +++ b/menus/full_nortikin.yaml @@ -446,6 +446,7 @@ - SvCircleApproxNode - SvSphereApproxNode - SvInscribedCircleNode + - SvSemiInscribedCircleNode - SvSteinerEllipseNode - --- - SvMeshSelectNodeMk2 diff --git a/nodes/analyzer/poly_inscribed_circle.py b/nodes/analyzer/poly_inscribed_circle.py new file mode 100644 index 0000000000..9f97d773c7 --- /dev/null +++ b/nodes/analyzer/poly_inscribed_circle.py @@ -0,0 +1,96 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np +import bpy +from bpy.props import BoolProperty, EnumProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.utils.inscribed_circle import calc_inscribed_circle, ERROR, RETURN_NONE, ASIS +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level + +class SvSemiInscribedCircleNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Polygon Inscribed Circle + Tooltip: Inscribed circle for an arbitrary convex polygon + """ + bl_idname = 'SvSemiInscribedCircleNode' + bl_label = 'Polygon Inscribed Circle' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_POLY_INSCRIBED_CIRCLE' + sv_dependencies = {'scipy'} + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices") + self.inputs.new('SvStringsSocket', "Faces") + self.outputs.new('SvMatrixSocket', "Center") + self.outputs.new('SvStringsSocket', "Radius") + + flat_output : BoolProperty( + name = "Flat Matrix output", + description = "Output single flat list of matrices", + default = True, + update = updateNode) + + concave_modes = [ + (RETURN_NONE, "Skip", "Skip concave faces - do not generate output for them", 0), + (ERROR, "Error", "Generate an error if encounter a concave face", 1), + (ASIS, "As Is", "Try to calculate inscribed circle anyway (it probably will be incorrect)", 2) + ] + + on_concave : EnumProperty( + name = "On concave face", + description = "What to do if encounter a concave face", + default = RETURN_NONE, + items = concave_modes, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'flat_output') + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.label(text = "On concave faces:") + layout.prop(self, 'on_concave', text='') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get() + vertices_s = ensure_nesting_level(vertices_s, 4) + faces_s = self.inputs['Faces'].sv_get() + faces_s = ensure_nesting_level(faces_s, 4) + + matrix_out = [] + radius_out = [] + for params in zip_long_repeat(vertices_s, faces_s): + new_matrix = [] + new_radius = [] + for vertices, faces in zip_long_repeat(*params): + vertices = np.array(vertices) + for face in faces: + face = np.array(face) + circle = calc_inscribed_circle(vertices[face], + on_concave = self.on_concave) + if circle is not None: + new_matrix.append(circle.get_matrix()) + new_radius.append(circle.radius) + if self.flat_output: + matrix_out.extend(new_matrix) + else: + matrix_out.append(new_matrix) + radius_out.append(new_radius) + + self.outputs['Center'].sv_set(matrix_out) + self.outputs['Radius'].sv_set(radius_out) + +def register(): + bpy.utils.register_class(SvSemiInscribedCircleNode) + +def unregister(): + bpy.utils.unregister_class(SvSemiInscribedCircleNode) + diff --git a/nodes/modifier_change/edge_split.py b/nodes/modifier_change/edge_split.py index f4c2bf8784..91af9ea947 100644 --- a/nodes/modifier_change/edge_split.py +++ b/nodes/modifier_change/edge_split.py @@ -36,8 +36,8 @@ class SvSplitEdgesMk3Node(ModifierNode, SverchCustomTreeNode, bpy.types.Node): def update_mode(self, context): self.inputs['Factor'].hide = False # This can be True in old nodes self.inputs['Cuts'].hide = False # This can be True in old nodes - self.inputs['Factor'].enabled = self.mode != 'MULTI' - self.inputs['Cuts'].enabled = self.mode == 'MULTI' + self.inputs['Factor'].hide_safe = self.mode != 'MULTI' + self.inputs['Cuts'].hide_safe = self.mode == 'MULTI' self.process_node(context) factor: bpy.props.FloatProperty( @@ -60,7 +60,7 @@ def sv_init(self, context): self.inputs.new('SvStringsSocket', 'Factor').prop_name = 'factor' s = self.inputs.new('SvStringsSocket', 'Cuts') s.prop_name = 'count' - s.enabled = False + #s.enabled = False self.outputs.new('SvVerticesSocket', 'Vertices') self.outputs.new('SvStringsSocket', 'Edges') self.outputs.new('SvStringsSocket', 'Faces') diff --git a/nodes/surface/intersect_curve_surface.py b/nodes/surface/intersect_curve_surface.py index d75d458ef4..83b80e8547 100644 --- a/nodes/surface/intersect_curve_surface.py +++ b/nodes/surface/intersect_curve_surface.py @@ -76,6 +76,7 @@ def sv_init(self, context): self.inputs.new('SvSurfaceSocket', "Surface") self.outputs.new('SvVerticesSocket', "Point") self.outputs.new('SvStringsSocket', "T") + self.outputs.new('SvVerticesSocket', "UVPoint") def process(self): if not any(socket.is_linked for socket in self.outputs): @@ -89,7 +90,8 @@ def process(self): tolerance = 10**(-self.accuracy) points_out = [] - u_out = [] + t_out = [] + uv_out = [] for surfaces, curves in zip_long_repeat(surfaces_s, curves_s): for surface, curve in zip_long_repeat(surfaces, curves): result = intersect_curve_surface(curve, surface, @@ -99,14 +101,17 @@ def process(self): raycast_method = self.raycast_method, support_nurbs = self.use_nurbs ) - new_points = [p[1] for p in result] - new_u = [p[0] for p in result] + new_points = result.points + new_t = result.ts + new_uv = [(u, v, 0) for u, v in zip(result.us, result.vs)] points_out.append(new_points) - u_out.append(new_u) + t_out.append(new_t) + uv_out.append(new_uv) self.outputs['Point'].sv_set(points_out) - self.outputs['T'].sv_set(u_out) - + self.outputs['T'].sv_set(t_out) + if 'UVPoint' in self.outputs: + self.outputs['UVPoint'].sv_set(uv_out) def register(): bpy.utils.register_class(SvExCrossCurveSurfaceNode) diff --git a/ui/icons/sv_poly_inscribed_circle.png b/ui/icons/sv_poly_inscribed_circle.png new file mode 100644 index 0000000000..d98f6b5ff0 Binary files /dev/null and b/ui/icons/sv_poly_inscribed_circle.png differ diff --git a/ui/icons/svg/sv_poly_inscribed_circle.svg b/ui/icons/svg/sv_poly_inscribed_circle.svg new file mode 100644 index 0000000000..620a5486eb --- /dev/null +++ b/ui/icons/svg/sv_poly_inscribed_circle.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/utils/geom.py b/utils/geom.py index 258e4998b6..83e51dc5dd 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -2801,3 +2801,24 @@ def scale_relative(points, center, scale): return (points + center).tolist() +def is_convex_2d(verts): + """ + Check if 2D polygon is convex. + + Args: + verts: np.array or list of shape (n,3); only first and second components are considered. + + Returns: + boolean. + """ + verts = np.array(verts) + edges = np.roll(verts, -1, axis=0) - verts + sign = None + for e1, e2 in zip(edges[:-1], edges[1:]): + n = np.cross(e1, e2) + if sign is None: + sign = n[2] + elif sign * n[2] < 0: + return False + return True + diff --git a/utils/inscribed_circle.py b/utils/inscribed_circle.py new file mode 100644 index 0000000000..2610581210 --- /dev/null +++ b/utils/inscribed_circle.py @@ -0,0 +1,133 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np +from mathutils import Vector, Matrix +from sverchok.utils.geom import linear_approximation, CircleEquation3D, is_convex_2d +from sverchok.utils.math import np_multiply_matrices_vectors, np_dot +from sverchok.dependencies import scipy +if scipy is not None: + from scipy.optimize import linprog + +RETURN_NONE = 'RETURN_NONE' +ERROR = 'ERROR' +ASIS = 'ASIS' + +def calc_inscribed_circle(verts, on_concave=ERROR): + """ + Calculate the biggest circle which can be inscribed into a polygon + (i.e. a circle which is inside the polygon, and touches some of polygon's edges). + + Only convex polygons are supported. + + Requires scipy. + + Theory: + If each edge of polygon (in 2D XOY plane) is represented by a unit vector (a, b), + which starts at polygon's vertex, then distance from this edge to arbitrary point (x,y) + inside polygon can be calculated as + + rho(edge, (x,y)) = a x + b y - d + + where d = a x0 + b y0, and (x0, y0) are coordinates of corresponding polygon vertex. + + Based on this formula, it is possible to state a linear programming problem. We + search for a point (x, y, Z), which satisfies constraints + + Z <= rho(edge_i, (x,y)) for i in 1 .. N, + + i.e. + + Z <= a_i x + b_i y - d_i + + or + + -a_i x -b_i y + Z <= -d_i + + with additional conditions: + + x >= 0, y >= 0, Z >= 0 + + and provides maximum for the goal function F(x, y, Z) = Z. + x and y will give us coordinates of circle's center. + + Also refer to: https://arxiv.org/pdf/1212.3193 . + + Arguments: + * verts - np.array (or list) of shape (n, 3), representing vertices of the polygon. + + Returns: an instance of CircleEquation3D. + """ + n = len(verts) + verts = np.array(verts) + + e1 = verts[1] - verts[0] + e2 = verts[2] - verts[1] + n1 = np.cross(e1, e2) + + approx = linear_approximation(verts) + plane = approx.most_similar_plane() + plane_matrix = plane.get_matrix() + + # Flip plane matrix, in case approximation gave us a plane + # with normal pointing in direction opposite to polygon normal. + plane_normal = np.array(plane.normal) + if np.dot(n1, plane_normal) < 0: + plane_matrix = Matrix.Diagonal(Vector([-1,-1,-1])) @ plane_matrix + verts = verts[::-1] + plane_normal = - plane_normal + + # Project all vertices to 2D space + inv = np.linalg.inv(plane_matrix.to_3x3()) + pt0 = np.array(approx.center) + verts2d = np_multiply_matrices_vectors(inv, verts - pt0) + + if on_concave != ASIS: + if not is_convex_2d(verts2d): + if on_concave == ERROR: + raise Exception("Polygon is not convex") + else: + return None + + # we will need only 2 coordinates + verts2d = verts2d[:,0:2] + # linprog method automatically assumes that all variables are non-negative. + # So move all vertices to first quadrant. + origin = verts2d.min(axis=0) + verts2d -= origin[:2] + + edges2d = np.roll(verts2d, -1, axis=0) - verts2d # (n, 2) + edges2d_normalized = edges2d / np.linalg.norm(edges2d, axis=1, keepdims=True) + + # edges line equations matrix + edges_eq = np.zeros((n+1, 3)) + edges_eq[:n,0] = edges2d_normalized[:,1] + edges_eq[:n,1] = -edges2d_normalized[:,0] + edges_eq[:n,2] = 1 + edges_eq[n,:] = np.array([0, 0, -1]) # -Z <= 0 + + # right-hand side of edges line equations + D = np_dot(-edges_eq[:n,:2], verts2d) + D = np.append(D, 0) + + # function to be minimized: it's -Z (we need to maximize Z). + c = np.array([0.0, 0.0, -1.0]) + + res = linprog(c, A_ub = edges_eq, b_ub = -D, method='highs') + center2d = res.x + if center2d is None: + return None + center2d[2] = 0 + + distances = np_dot(-edges_eq[:n,:2], center2d[:2]) - D[:n] + rho = abs(distances).min() + o = np.zeros((3,)) + o[:2] = origin + # Return to 3D space + center = plane_matrix @ (Vector(center2d) + Vector(o)) + Vector(pt0) + return CircleEquation3D.from_center_radius_normal(center, rho, plane_normal) + diff --git a/utils/manifolds.py b/utils/manifolds.py index d5c873ca7c..cbcc56e50a 100644 --- a/utils/manifolds.py +++ b/utils/manifolds.py @@ -809,6 +809,19 @@ def raycast_surface(surface, src_points, directions, samples=50, precise=True, c raycaster.init_bvh(samples) return raycaster.raycast(src_points, directions, precise=precise, calc_points=calc_points, method=method, on_init_fail=on_init_fail) +class CurveSurfaceIntersections: + def __init__(self): + self.ts = [] + self.us = [] + self.vs = [] + self.points = [] + + def add(self, t, u, v, point): + self.ts.append(t) + self.us.append(u) + self.vs.append(v) + self.points.append(point) + def intersect_curve_surface(curve, surface, init_samples=10, raycast_samples=10, tolerance=1e-3, maxiter=50, raycast_method='hybr', support_nurbs=False): """ Intersect a curve with a surface. @@ -840,17 +853,17 @@ def do_raycast(point, tangent, sign=1): u_range = np.linspace(u_min, u_max, num=init_samples) points = curve.evaluate_array(u_range) tangents = curve.tangent_array(u_range) - for u1, u2, p1, p2, tangent1, tangent2 in zip(u_range, u_range[1:], points, points[1:], tangents,tangents[1:]): + for t1, t2, p1, p2, tangent1, tangent2 in zip(u_range, u_range[1:], points, points[1:], tangents,tangents[1:]): raycast = raycaster.raycast([p1, p2], [tangent1, -tangent2], precise = False, calc_points=False, on_init_fail = RETURN_NONE) if raycast is None: continue - good_ranges.append((u1, u2, raycast.points[0], raycast.points[1])) + good_ranges.append((t1, t2, raycast.points[0], raycast.points[1])) - def to_curve(point, curve, u1, u2, raycast=None): + def to_curve(point, curve, t1, t2, raycast=None): if support_nurbs and is_nurbs and raycast is not None: - segment = curve.cut_segment(u1, u2) + segment = curve.cut_segment(t1, t2) surface_u, surface_v = raycast.us[0], raycast.vs[0] point_on_surface = raycast.points[0] surface_normal = surface.normal(surface_u, surface_v) @@ -865,7 +878,7 @@ def to_curve(point, curve, u1, u2, raycast=None): return r[0] else: ortho = ortho_project_curve(point, curve, - subdomain = (u1, u2), + subdomain = (t1, t2), init_samples = 2, on_fail = RETURN_NONE) if ortho is None: @@ -873,17 +886,19 @@ def to_curve(point, curve, u1, u2, raycast=None): else: return ortho.nearest_u, ortho.nearest - result = [] - for u1, u2, init_p1, init_p2 in good_ranges: + result = CurveSurfaceIntersections() + for t1, t2, init_p1, init_p2 in good_ranges: - tangent = curve.tangent(u1) - point = curve.evaluate(u1) + tangent = curve.tangent(t1) + point = curve.evaluate(t1) i = 0 sign = 1 prev_prev_point = None prev_point = init_p1 - u_root = None + t_root = None + u_value = None + v_value = None point_found = False raycast = None while True: @@ -891,11 +906,11 @@ def to_curve(point, curve, u1, u2, raycast=None): if i > maxiter: raise Exception("Maximum number of iterations is exceeded; last step {} - {} = {}".format(prev_prev_point, point, step)) - on_curve = to_curve(prev_point, curve, u1, u2, raycast=raycast) + on_curve = to_curve(prev_point, curve, t1, t2, raycast=raycast) if on_curve is None: break - u_root, point = on_curve - if u_root < u1 or u_root > u2: + t_root, point = on_curve + if t_root < t1 or t_root > t2: break step = np.linalg.norm(point - prev_point) if step < tolerance and i > 1: @@ -904,17 +919,19 @@ def to_curve(point, curve, u1, u2, raycast=None): break prev_point = point - tangent = curve.tangent(u_root) + tangent = curve.tangent(t_root) sign, raycast = do_raycast(point, tangent, sign) if raycast is None: raise Exception("Iteration #{}: Can't do a raycast with point {}, direction {} onto surface {}".format(i, point, tangent, surface)) point = raycast.points[0] + u_value = raycast.us[0] + v_value = raycast.vs[0] step = np.linalg.norm(point - prev_point) prev_prev_point = prev_point prev_point = point if point_found: - result.append((u_root, point)) + result.add(t_root, u_value, v_value, point) return result