From 5de2533670944301e85d24b46f87e316a6695ff6 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Oct 2019 20:26:39 +0500 Subject: [PATCH 01/11] Voronoi mode upgrade. --- nodes/modifier_make/voronoi_2d.py | 316 ++++++++++++++++++++++++++---- utils/geom.py | 187 +++++++++++++++++- utils/voronoi.py | 2 +- 3 files changed, 468 insertions(+), 37 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index 8b3c879f7..3ab958e65 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -16,12 +16,195 @@ # # ##### END GPL LICENSE BLOCK ##### +from math import sqrt +from collections import defaultdict + import bpy -from bpy.props import FloatProperty +from bpy.props import FloatProperty, EnumProperty +from mathutils import Vector +from mathutils.geometry import intersect_line_line_2d from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode -from sverchok.utils.voronoi import Site, computeVoronoiDiagram, computeDelaunayTriangulation +from sverchok.utils.voronoi import Site, computeVoronoiDiagram, computeDelaunayTriangulation, BIG_FLOAT +from sverchok.utils.geom import center, LineEquation2D, CircleEquation2D +from sverchok.utils.sv_bmesh_utils import pydata_from_bmesh, bmesh_from_pydata +from sverchok.utils.logging import debug, info + +class Bounds(object): + def __init__(self): + self.x_max = 0 + self.y_max = 0 + self.x_min = 0 + self.y_min = 0 + self.r_max = 0 + self.center = (0,0) + + @classmethod + def new(cls, mode): + if mode == 'BOX': + return BoxBounds() + elif mode == 'CIRCLE': + return CircleBounds() + else: + raise Exception("Unknown bounds type") + +class Mesh2D(object): + def __init__(self): + self.verts = [] + self.all_edges = [] + self.linked_verts = defaultdict(set) + self._next_vert = 0 + + @classmethod + def from_pydata(cls, verts, edges): + mesh = Mesh2D() + for vert in verts: + mesh.new_vert(vert) + for i, j in edges: + mesh.new_edge(i, j) + return mesh + + def new_vert(self, vert): + if vert is None: + raise Exception("new_vert(None)") + if vert[0] is None or vert[1] is None: + raise Exception(f"new_vert({vert})") + self.verts.append(vert) + idx = self._next_vert + self._next_vert += 1 + return idx + + def new_edge(self, i, j): + v1, v2 = self.verts[i], self.verts[j] + #info("Add: %s (%s) => %s (%s)", i, v1, j, v2) + self.all_edges.append((v1, v2)) + self.linked_verts[i].add(j) + self.linked_verts[j].add(i) + + def remove_edge(self, i, j): + if (self.verts[i], self.verts[j]) in self.all_edges: + self.all_edges.remove((self.verts[i], self.verts[j])) + if (self.verts[j], self.verts[i]) in self.all_edges: + self.all_edges.remove((self.verts[j], self.verts[i])) + if j in self.linked_verts[i]: + self.linked_verts[i].remove(j) + if i in self.linked_verts[j]: + self.linked_verts[j].remove(i) + + def remove_vert(self, vert): + self.verts.remove(vert) + + def to_pydata(self): + verts = [vert for vert in self.verts if vert is not None] + lut = dict((vert, idx) for idx, vert in enumerate(verts)) + #info(lut) + edges = [] + for v1, v2 in self.all_edges: + i1 = lut.get(v1, None) + i2 = lut.get(v2, None) + #info("Get: %s (%s) => %s (%s)", v1, i1, v2, i2) + if i1 is not None and i2 is not None: + edges.append((i1, i2)) + + return verts, edges + +class BoxBounds(Bounds): + + def contains(self, p): + x, y = tuple(p) + return (self.x_min <= x <= self.x_max) and (self.y_min <= y <= self.y_max) + + def segment_intersection(self, p1, p2): + if not isinstance(p1, Vector): + p1 = Vector(p1) + if not isinstance(p2, Vector): + p2 = Vector(p2) + + v1 = (self.x_min, self.y_min) + v2 = (self.x_min, self.y_max) + v3 = (self.x_max, self.y_max) + v4 = (self.x_max, self.y_min) + + e1 = (v1, v2) + e2 = (v2, v3) + e3 = (v3, v4) + e4 = (v4, v1) + + min_r = BIG_FLOAT + nearest = None + + for v_i, v_j in [e1, e2, e3, e4]: + intersection = intersect_line_line_2d(p1, p2, v_i, v_j) + if intersection is not None: + r = (p1 - intersection).length + if r < min_r: + nearest = intersection + min_r = r + + return nearest + + def line_intersection(self, p, line): + if not isinstance(p, Vector): + p = Vector(p) + + v1 = (self.x_min, self.y_min) + v2 = (self.x_min, self.y_max) + v3 = (self.x_max, self.y_max) + v4 = (self.x_max, self.y_min) + + e1 = (v1, v2) + e2 = (v2, v3) + e3 = (v3, v4) + e4 = (v4, v1) + + min_r = BIG_FLOAT + nearest = None + + for v_i, v_j in [e1, e2, e3, e4]: + bound = LineEquation2D.from_two_points(v_i, v_j) + intersection = bound.intersect_with_line(line) + if intersection is not None: + r = (p - intersection).length + #info("INT: [%s - %s] X [%s] => %s (%s)", v_i, v_j, line, intersection, r) + if r < min_r: + nearest = intersection + min_r = r + + return nearest + +class CircleBounds(Bounds): + + @property + def circle(self): + return CircleEquation2D(self.center, self.r_max) + + def contains(self, p): + return self.circle.contains(p) + + def segment_intersection(self, p1, p2): + r = self.circle.intersect_with_segment(p1, p2) + if r is None: + return None + if r[0] is None and r[1] is None: + return None + if r[0] is not None: + return r[0] + if r[1] is not None: + return r[1] + + def line_intersection(self, p, line): + intersection = self.circle.intersect_with_line(line) + if intersection is None: + return None + else: + v1, v2 = intersection + r1 = (p - v1).length + r2 = (p - v2).length + if r1 < r2: + return v1 + else: + return v2 class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): ''' vr Voronoi 2d line ''' @@ -34,14 +217,34 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): name='clip', description='Clipping Distance', default=1.0, min=0, update=updateNode) + bound_modes = [ + ('BOX', 'Bounding Box', "Bounding Box", 0), + ('CIRCLE', 'Circle', "Circle", 1) + ] + + bound_mode: EnumProperty( + name = 'Bounds Mode', + description = "Bounding mode", + items = bound_modes, + default = 'BOX', + update = updateNode) + def sv_init(self, context): self.inputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvStringsSocket', "Edges") def draw_buttons(self, context, layout): + layout.prop(self, "bound_mode") layout.prop(self, "clip", text="Clipping") + def lines_from_polygons(self, polygons): + result = set() + for idx in polygons.keys(): + for line in polygons[idx]: + result.add(line) + return list(result) + def process(self): if not self.inputs['Vertices'].is_linked: @@ -56,47 +259,90 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): # polys_out = [] edges_out = [] for obj in points_in: + bounds = Bounds.new(self.bound_mode) pt_list = [] - x_max = obj[0][0] - x_min = obj[0][0] - y_min = obj[0][1] - y_max = obj[0][1] + bounds.x_max = obj[0][0] + bounds.x_min = obj[0][0] + bounds.y_min = obj[0][1] + bounds.y_max = obj[0][1] + x0, y0, z0 = center(obj) + bounds.center = (x0, y0) # creates points in format for voronoi library, throwing away z - for pt in obj: - x, y = pt[0], pt[1] - x_max = max(x, x_max) - x_min = min(x, x_min) - y_max = max(y, y_max) - y_min = min(x, x_min) - pt_list.append(Site(pt[0], pt[1])) + for x, y, z in obj: + r = sqrt((x-x0)**2 + (y-y0)**2) + bounds.r_max = max(r, bounds.r_max) + bounds.x_max = max(x, bounds.x_max) + bounds.x_min = min(x, bounds.x_min) + bounds.y_max = max(y, bounds.y_max) + bounds.y_min = min(x, bounds.x_min) + pt_list.append(Site(x, y)) - res = computeVoronoiDiagram(pt_list) + verts, lines, all_edges = computeVoronoiDiagram(pt_list) + finite_edges = [(edge[1], edge[2]) for edge in all_edges if -1 not in edge] + bm = Mesh2D.from_pydata(verts, finite_edges) + + infinite_lines = defaultdict(list) + for line_index, i1, i2 in all_edges: + line = lines[line_index] + a, b, c = line + if i1 == -1: + eqn = LineEquation2D(a, b, -c) + infinite_lines[i2].append(eqn) + elif i2 == -1: + eqn = LineEquation2D(a, b, -c) + infinite_lines[i1].append(eqn) - edges = res[2] delta = self.clip - x_max = x_max + delta - y_max = y_max + delta + bounds.x_max = bounds.x_max + delta + bounds.y_max = bounds.y_max + delta + + bounds.x_min = bounds.x_min - delta + bounds.y_min = bounds.y_min - delta - x_min = x_min - delta - y_min = y_min - delta + bounds.r_max = bounds.r_max + delta # clipping box to bounding box. - pts_tmp = [] - for pt in res[0]: - x, y = pt[0], pt[1] - if x < x_min: - x = x_min - if x > x_max: - x = x_max - - if y < y_min: - y = y_min - if y > y_max: - y = y_max - pts_tmp.append((x, y, 0)) - - pts_out.append(pts_tmp) - edges_out.append([(edge[1], edge[2]) for edge in edges if -1 not in edge]) + verts_to_remove = set() + edges_to_remove = set() + + for vert_idx, vert in enumerate(bm.verts[:]): + x, y = tuple(vert) + if not bounds.contains((x,y)): + verts_to_remove.add(vert_idx) + for other_vert_idx in list(bm.linked_verts[vert_idx]): + edges_to_remove.add((vert_idx, other_vert_idx)) + other_vert = bm.verts[other_vert_idx] + if other_vert is not None: + x2, y2 = tuple(other_vert) + intersection = bounds.segment_intersection((x,y), (x2,y2)) + if intersection is not None: + x_i, y_i = tuple(intersection) + new_vert_idx = bm.new_vert((x_i, y_i)) + #info("CLIP: Added point: %s => %s", (x_i, y_i), new_vert_idx) + bm.new_edge(other_vert_idx, new_vert_idx) + + for vert_index in infinite_lines.keys(): + x,y = bm.verts[vert_index] + vert = Vector((x,y)) + if vert_index not in verts_to_remove: + for line in infinite_lines[vert_index]: + intersection = bounds.line_intersection(vert, line) + x_i, y_i = tuple(intersection) + new_vert_idx = bm.new_vert((x_i, y_i)) + #info("INF: Added point: %s: %s => %s", (x,y), (x_i, y_i), new_vert_idx) + bm.new_edge(vert_index, new_vert_idx) + + for i, j in edges_to_remove: + bm.remove_edge(i, j) + for vert_idx in verts_to_remove: + bm.verts[vert_idx] = None + + verts, edges = bm.to_pydata() + + verts3d = [(vert[0], vert[1], 0) for vert in verts] + pts_out.append(verts3d) + edges_out.append(edges) + #edges_out.append(finite_edges) # outputs self.outputs['Vertices'].sv_set(pts_out) diff --git a/utils/geom.py b/utils/geom.py index 9df937612..1d57c1055 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -27,7 +27,7 @@ only for speed, never for aesthetics or line count or cleverness. ''' import math -from math import sin, cos +from math import sin, cos, sqrt import numpy as np from numpy import linalg from functools import wraps @@ -1444,6 +1444,191 @@ class LineEquation(object): # Then find an intersection of that plane with this line. return plane.intersect_with_line(self) +class LineEquation2D(object): + def __init__(self, a, b, c): + epsilon = 1e-8 + if abs(a) < epsilon and abs(b) < epsilon: + raise Exception(f"Direction is (nearly) zero: {a}, {b}") + self.a = a + self.b = b + self.c = c + + def __str__(self): + return f"{self.a}*x + {self.b}*y + {self.c} = 0" + + @classmethod + def from_normal_and_point(cls, normal, point): + a, b = tuple(normal) + cx, cy = tuple(point) + c = - (a*cx + b*cy) + return LineEquation2D(a, b, c) + + @classmethod + def from_direction_and_point(cls, direction, point): + dx, dy = tuple(direction) + return LineEquation2D.from_normal_and_point((-dy, dx), point) + + @classmethod + def from_two_points(cls, v1, v2): + x1,y1 = tuple(v1) + x2,y2 = tuple(v2) + a = y2 - y1 + b = x1 - x2 + c = y1*x2 - x1*y2 + epsilon = 1e-8 + if abs(a) < epsilon and abs(b) < epsilon: + raise Exception(f"Two points are too close: {v1}, {v2}") + return LineEquation2D(a, b, c) + + @classmethod + def from_coordinate_axis(cls, axis_name): + if axis_name == 'X': + return LineEquation2D(0, 1, 0) + elif axis_name == 'Y': + return LineEquation2D(1, 0, 0) + else: + raise Exception("Unknown coordinate axis name") + + @property + def normal(self): + return Vector((self.a, self.b)) + + @normal.setter + def normal(self, normal): + self.a = normal[0] + self.b = normal[1] + + @property + def direction(self): + return Vector((-self.b, self.a)) + + @direction.setter + def direction(self, direction): + self.a = - direvtion[1] + self.b = direction[0] + + def nearest_point_to_origin(self): + a, b, c = self.a, self.b, self.c + sqr = a*a + b*b + return Vector(( (-a*c)/sqr, (-b*c)/sqr )) + + def two_points(self): + p1 = self.nearest_point_to_origin() + p2 = p1 + self.direction + return p1, p2 + + def check(self, point, eps=1e-6): + a, b, c = self.a, self.b, self.c + x, y, z = tuple(point) + value = a*x + b*y + c + return abs(value) < eps + + def side_of_point(self, point, eps=1e-8): + a, b, c = self.a, self.b, self.c + x, y, z = tuple(point) + value = a*x + b*y + c + if abs(value) < eps: + return 0 + elif value > 0: + return +1 + else: + return -1 + + def distance_to_point(self, point): + a, b, c = self.a, self.b, self.c + x, y, z = tuple(point) + value = a*x + b*y + c + numerator = abs(value) + denominator = sqrt(a*a + b*b) + return numerator / denominator + + def projection_of_point(self, point): + normal = self.normal.normalized() + distance = self.distance_to_point(point) + sign = self.side_of_point(point) + return Vector(point) - sign * distance * normal + + def intersect_with_line(self, line2, min_det=1e-8): + """ + Find intersection between two lines. + """ + # + # / + # | A1 x + B1 y + C1 = 0 + # / + # \ + # | A2 x + B2 y + C2 = 0 + # \ + # + matrix = np.array([ + [self.a, self.b], + [line2.a, line2.b] + ]) + free = np.array([ + -self.c, + -line2.c + ]) + + det = linalg.det(matrix) + if abs(det) < min_det: + return None + + result = np.linalg.solve(matrix, free) + x, y = tuple(result) + return Vector((x, y)) + +class CircleEquation2D(object): + def __init__(self, center, radius): + if not isinstance(center, Vector): + center = Vector(center) + self.center = center + self.radius = radius + + def __str__(self): + return f"(x - {self.center.x})^2 + (y - {self.center.y})^2 = {self.radius}^2" + + def evaluate(self, point): + x, y = tuple(point) + x0, y0 = tuple(self.center) + r = self.radius + return (x - x0)**2 + (y - y0)**2 - r**2 + + def check(self, point, eps=1e-8): + value = self.evaluate(point) + return abs(value) < eps + + def intersect_with_line(self, line2): + line_p1, line_p2 = line2.two_points() + r = mathutils.geometry.intersect_line_sphere_2d(line_p1, line_p2, self.center, self.radius, False) + return r + + def intersect_with_segment(self, p1, p2): + return mathutils.geometry.intersect_line_sphere_2d(p1, p2, self.center, self.radius, True) + + def intersect_with_circle(self, circle2): + return mathutils.geometry.intersect_sphere_sphere_2d(self.center, self.radius, circle2.center, circle2.radius) + + def projection_of_point(self, point, nearest=True): + line = LineEquation2D.from_two_points(self.center, point) + p1, p2 = self.intersect_with_line(line) + if nearest: + rho1 = (point - p1).length + rho2 = (point - p2).length + if rho1 < rho2: + return p1 + else: + return p2 + else: + return p1, p2 + + def contains(self, point, include_bound=True, eps=1e-8): + value = self.evaluate(point) + on_edge = abs(value) < eps + if include_bound: + return (value < 0) or on_edge + else: + return value < 0 + class LinearApproximationData(object): """ This class contains results of linear approximation calculation. diff --git a/utils/voronoi.py b/utils/voronoi.py index 08131de5f..0d66cc8a3 100644 --- a/utils/voronoi.py +++ b/utils/voronoi.py @@ -791,7 +791,7 @@ def computeVoronoiDiagram(points): context = Context() context.triangulate = True voronoi(siteList,context) - return (context.vertices,context.polygons,context.edges) + return (context.vertices,context.lines,context.edges) #------------------------------------------------------------------ def computeDelaunayTriangulation(points): -- GitLab From 90c2a97040623d933ab683bab21318cf42b03ee7 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Oct 2019 21:10:57 +0500 Subject: [PATCH 02/11] Work with infinite lines. --- nodes/modifier_make/voronoi_2d.py | 91 +++++++++++++++++++------------ 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index 3ab958e65..f2c67da18 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -52,7 +52,7 @@ class Bounds(object): class Mesh2D(object): def __init__(self): self.verts = [] - self.all_edges = [] + self.all_edges = set() self.linked_verts = defaultdict(set) self._next_vert = 0 @@ -78,7 +78,7 @@ class Mesh2D(object): def new_edge(self, i, j): v1, v2 = self.verts[i], self.verts[j] #info("Add: %s (%s) => %s (%s)", i, v1, j, v2) - self.all_edges.append((v1, v2)) + self.all_edges.add((v1, v2)) self.linked_verts[i].add(j) self.linked_verts[j].add(i) @@ -114,13 +114,9 @@ class BoxBounds(Bounds): def contains(self, p): x, y = tuple(p) return (self.x_min <= x <= self.x_max) and (self.y_min <= y <= self.y_max) - - def segment_intersection(self, p1, p2): - if not isinstance(p1, Vector): - p1 = Vector(p1) - if not isinstance(p2, Vector): - p2 = Vector(p2) - + + @property + def edges(self): v1 = (self.x_min, self.y_min) v2 = (self.x_min, self.y_max) v3 = (self.x_max, self.y_max) @@ -131,10 +127,18 @@ class BoxBounds(Bounds): e3 = (v3, v4) e4 = (v4, v1) + return [e1, e2, e3, e4] + + def segment_intersection(self, p1, p2): + if not isinstance(p1, Vector): + p1 = Vector(p1) + if not isinstance(p2, Vector): + p2 = Vector(p2) + min_r = BIG_FLOAT nearest = None - for v_i, v_j in [e1, e2, e3, e4]: + for v_i, v_j in self.edges: intersection = intersect_line_line_2d(p1, p2, v_i, v_j) if intersection is not None: r = (p1 - intersection).length @@ -144,24 +148,14 @@ class BoxBounds(Bounds): return nearest - def line_intersection(self, p, line): + def ray_intersection(self, p, line): if not isinstance(p, Vector): p = Vector(p) - v1 = (self.x_min, self.y_min) - v2 = (self.x_min, self.y_max) - v3 = (self.x_max, self.y_max) - v4 = (self.x_max, self.y_min) - - e1 = (v1, v2) - e2 = (v2, v3) - e3 = (v3, v4) - e4 = (v4, v1) - min_r = BIG_FLOAT nearest = None - for v_i, v_j in [e1, e2, e3, e4]: + for v_i, v_j in self.edges: bound = LineEquation2D.from_two_points(v_i, v_j) intersection = bound.intersect_with_line(line) if intersection is not None: @@ -173,6 +167,18 @@ class BoxBounds(Bounds): return nearest + def line_intersection(self, line): + result = [] + eps = 1e-8 + for v_i, v_j in self.edges: + bound = LineEquation2D.from_two_points(v_i, v_j) + intersection = bound.intersect_with_line(line) + if intersection is not None: + x,y = tuple(intersection) + if (self.x_min-eps <= x <= self.x_max+eps) and (self.y_min-eps <= y <= self.y_max+eps): + result.append(intersection) + return result + class CircleBounds(Bounds): @property @@ -193,7 +199,7 @@ class CircleBounds(Bounds): if r[1] is not None: return r[1] - def line_intersection(self, p, line): + def ray_intersection(self, p, line): intersection = self.circle.intersect_with_line(line) if intersection is None: return None @@ -206,6 +212,10 @@ class CircleBounds(Bounds): else: return v2 + def line_intersection(self, line): + intersection = self.circle.intersect_with_line(line) + return intersection + class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): ''' vr Voronoi 2d line ''' bl_idname = 'Voronoi2DNode' @@ -281,16 +291,19 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): finite_edges = [(edge[1], edge[2]) for edge in all_edges if -1 not in edge] bm = Mesh2D.from_pydata(verts, finite_edges) - infinite_lines = defaultdict(list) + infinite_lines = [] + rays = defaultdict(list) for line_index, i1, i2 in all_edges: - line = lines[line_index] - a, b, c = line - if i1 == -1: + if i1 == -1 or i2 == -1: + line = lines[line_index] + a, b, c = line eqn = LineEquation2D(a, b, -c) - infinite_lines[i2].append(eqn) - elif i2 == -1: - eqn = LineEquation2D(a, b, -c) - infinite_lines[i1].append(eqn) + if i1 == -1 and i2 != -1: + rays[i2].append(eqn) + elif i2 == -1 and i1 != -1: + rays[i1].append(eqn) + elif i1 == -1 and i2 == -1: + infinite_lines.append(eqn) delta = self.clip bounds.x_max = bounds.x_max + delta @@ -321,17 +334,27 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): #info("CLIP: Added point: %s => %s", (x_i, y_i), new_vert_idx) bm.new_edge(other_vert_idx, new_vert_idx) - for vert_index in infinite_lines.keys(): + for vert_index in rays.keys(): x,y = bm.verts[vert_index] vert = Vector((x,y)) if vert_index not in verts_to_remove: - for line in infinite_lines[vert_index]: - intersection = bounds.line_intersection(vert, line) + for line in rays[vert_index]: + intersection = bounds.ray_intersection(vert, line) x_i, y_i = tuple(intersection) new_vert_idx = bm.new_vert((x_i, y_i)) #info("INF: Added point: %s: %s => %s", (x,y), (x_i, y_i), new_vert_idx) bm.new_edge(vert_index, new_vert_idx) + for eqn in infinite_lines: + intersections = bounds.line_intersection(eqn) + if len(intersections) == 2: + v1, v2 = intersections + new_vert_1_idx = bm.new_vert(tuple(v1)) + new_vert_2_idx = bm.new_vert(tuple(v2)) + bm.new_edge(new_vert_1_idx, new_vert_2_idx) + else: + self.error("unexpected number of intersections of infinite line %s with area bounds: %s", eqn, intersections) + for i, j in edges_to_remove: bm.remove_edge(i, j) for vert_idx in verts_to_remove: -- GitLab From 571bd9ddf9f52888586558416de83f03a762ea01 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Oct 2019 21:46:09 +0500 Subject: [PATCH 03/11] minor code formatting. --- nodes/modifier_make/voronoi_2d.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index f2c67da18..bb805b64e 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -92,9 +92,6 @@ class Mesh2D(object): if i in self.linked_verts[j]: self.linked_verts[j].remove(i) - def remove_vert(self, vert): - self.verts.remove(vert) - def to_pydata(self): verts = [vert for vert in self.verts if vert is not None] lut = dict((vert, idx) for idx, vert in enumerate(verts)) @@ -287,11 +284,22 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): bounds.y_min = min(x, bounds.x_min) pt_list.append(Site(x, y)) + delta = self.clip + bounds.x_max = bounds.x_max + delta + bounds.y_max = bounds.y_max + delta + + bounds.x_min = bounds.x_min - delta + bounds.y_min = bounds.y_min - delta + + bounds.r_max = bounds.r_max + delta + verts, lines, all_edges = computeVoronoiDiagram(pt_list) finite_edges = [(edge[1], edge[2]) for edge in all_edges if -1 not in edge] bm = Mesh2D.from_pydata(verts, finite_edges) + # Diagram lines that go infinitely from one side of diagram to another infinite_lines = [] + # Lines that start at the one vertex of the diagram and go to infinity rays = defaultdict(list) for line_index, i1, i2 in all_edges: if i1 == -1 or i2 == -1: @@ -305,15 +313,6 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): elif i1 == -1 and i2 == -1: infinite_lines.append(eqn) - delta = self.clip - bounds.x_max = bounds.x_max + delta - bounds.y_max = bounds.y_max + delta - - bounds.x_min = bounds.x_min - delta - bounds.y_min = bounds.y_min - delta - - bounds.r_max = bounds.r_max + delta - # clipping box to bounding box. verts_to_remove = set() edges_to_remove = set() -- GitLab From 486ee7656d17df15e40c1ca690bcd32b585a2d52 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Oct 2019 21:58:36 +0500 Subject: [PATCH 04/11] Draw bounding edges. --- nodes/modifier_make/voronoi_2d.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index bb805b64e..3c09817a7 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -from math import sqrt +from math import sqrt, atan2 from collections import defaultdict import bpy @@ -316,6 +316,7 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): # clipping box to bounding box. verts_to_remove = set() edges_to_remove = set() + bounding_verts = [] for vert_idx, vert in enumerate(bm.verts[:]): x, y = tuple(vert) @@ -328,8 +329,9 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): x2, y2 = tuple(other_vert) intersection = bounds.segment_intersection((x,y), (x2,y2)) if intersection is not None: - x_i, y_i = tuple(intersection) - new_vert_idx = bm.new_vert((x_i, y_i)) + intersection = tuple(intersection) + new_vert_idx = bm.new_vert(intersection) + bounding_verts.append(new_vert_idx) #info("CLIP: Added point: %s => %s", (x_i, y_i), new_vert_idx) bm.new_edge(other_vert_idx, new_vert_idx) @@ -339,8 +341,9 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): if vert_index not in verts_to_remove: for line in rays[vert_index]: intersection = bounds.ray_intersection(vert, line) - x_i, y_i = tuple(intersection) - new_vert_idx = bm.new_vert((x_i, y_i)) + intersection = tuple(intersection) + new_vert_idx = bm.new_vert(intersection) + bounding_verts.append(new_vert_idx) #info("INF: Added point: %s: %s => %s", (x,y), (x_i, y_i), new_vert_idx) bm.new_edge(vert_index, new_vert_idx) @@ -350,10 +353,17 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): v1, v2 = intersections new_vert_1_idx = bm.new_vert(tuple(v1)) new_vert_2_idx = bm.new_vert(tuple(v2)) + bounding_verts.append(new_vert_1_idx) + bounding_verts.append(new_vert_2_idx) bm.new_edge(new_vert_1_idx, new_vert_2_idx) else: self.error("unexpected number of intersections of infinite line %s with area bounds: %s", eqn, intersections) + bounding_verts.sort(key = lambda idx: atan2(bm.verts[idx][1], bm.verts[idx][0])) + for i, j in zip(bounding_verts, bounding_verts[1:]): + bm.new_edge(i, j) + bm.new_edge(bounding_verts[-1], bounding_verts[0]) + for i, j in edges_to_remove: bm.remove_edge(i, j) for vert_idx in verts_to_remove: -- GitLab From 37e87f25bc4a0453c9b2bce3e643cb9e74f83c4a Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Oct 2019 22:04:29 +0500 Subject: [PATCH 05/11] An option to draw bounding edges. --- nodes/modifier_make/voronoi_2d.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index 3c09817a7..e4c5ad043 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -20,7 +20,7 @@ from math import sqrt, atan2 from collections import defaultdict import bpy -from bpy.props import FloatProperty, EnumProperty +from bpy.props import FloatProperty, EnumProperty, BoolProperty from mathutils import Vector from mathutils.geometry import intersect_line_line_2d @@ -236,6 +236,12 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): default = 'BOX', update = updateNode) + draw_bounds: BoolProperty( + name = "Draw Bounds", + description = "Draw bounding edges", + default = True, + update = updateNode) + def sv_init(self, context): self.inputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvVerticesSocket', "Vertices") @@ -243,15 +249,9 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons(self, context, layout): layout.prop(self, "bound_mode") + layout.prop(self, "draw_bounds") layout.prop(self, "clip", text="Clipping") - def lines_from_polygons(self, polygons): - result = set() - for idx in polygons.keys(): - for line in polygons[idx]: - result.add(line) - return list(result) - def process(self): if not self.inputs['Vertices'].is_linked: @@ -359,10 +359,11 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): else: self.error("unexpected number of intersections of infinite line %s with area bounds: %s", eqn, intersections) - bounding_verts.sort(key = lambda idx: atan2(bm.verts[idx][1], bm.verts[idx][0])) - for i, j in zip(bounding_verts, bounding_verts[1:]): - bm.new_edge(i, j) - bm.new_edge(bounding_verts[-1], bounding_verts[0]) + if self.draw_bounds: + bounding_verts.sort(key = lambda idx: atan2(bm.verts[idx][1], bm.verts[idx][0])) + for i, j in zip(bounding_verts, bounding_verts[1:]): + bm.new_edge(i, j) + bm.new_edge(bounding_verts[-1], bounding_verts[0]) for i, j in edges_to_remove: bm.remove_edge(i, j) -- GitLab From 51ab732ec72cdb364e5b329490cbea2d0fc35a04 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Oct 2019 22:24:07 +0500 Subject: [PATCH 06/11] Add an option to draw hanging lines. --- nodes/modifier_make/voronoi_2d.py | 114 ++++++++++++++++-------------- 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index e4c5ad043..55fcba1ce 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -242,6 +242,12 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): default = True, update = updateNode) + draw_hangs: BoolProperty( + name = "Draw Tails", + description = "Draw lines that end outside of clipping area", + default = True, + update = updateNode) + def sv_init(self, context): self.inputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvVerticesSocket', "Vertices") @@ -250,6 +256,8 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons(self, context, layout): layout.prop(self, "bound_mode") layout.prop(self, "draw_bounds") + if not self.draw_bounds: + layout.prop(self, "draw_hangs") layout.prop(self, "clip", text="Clipping") def process(self): @@ -297,22 +305,6 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): finite_edges = [(edge[1], edge[2]) for edge in all_edges if -1 not in edge] bm = Mesh2D.from_pydata(verts, finite_edges) - # Diagram lines that go infinitely from one side of diagram to another - infinite_lines = [] - # Lines that start at the one vertex of the diagram and go to infinity - rays = defaultdict(list) - for line_index, i1, i2 in all_edges: - if i1 == -1 or i2 == -1: - line = lines[line_index] - a, b, c = line - eqn = LineEquation2D(a, b, -c) - if i1 == -1 and i2 != -1: - rays[i2].append(eqn) - elif i2 == -1 and i1 != -1: - rays[i1].append(eqn) - elif i1 == -1 and i2 == -1: - infinite_lines.append(eqn) - # clipping box to bounding box. verts_to_remove = set() edges_to_remove = set() @@ -324,42 +316,60 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): verts_to_remove.add(vert_idx) for other_vert_idx in list(bm.linked_verts[vert_idx]): edges_to_remove.add((vert_idx, other_vert_idx)) - other_vert = bm.verts[other_vert_idx] - if other_vert is not None: - x2, y2 = tuple(other_vert) - intersection = bounds.segment_intersection((x,y), (x2,y2)) - if intersection is not None: - intersection = tuple(intersection) - new_vert_idx = bm.new_vert(intersection) - bounding_verts.append(new_vert_idx) - #info("CLIP: Added point: %s => %s", (x_i, y_i), new_vert_idx) - bm.new_edge(other_vert_idx, new_vert_idx) - - for vert_index in rays.keys(): - x,y = bm.verts[vert_index] - vert = Vector((x,y)) - if vert_index not in verts_to_remove: - for line in rays[vert_index]: - intersection = bounds.ray_intersection(vert, line) - intersection = tuple(intersection) - new_vert_idx = bm.new_vert(intersection) - bounding_verts.append(new_vert_idx) - #info("INF: Added point: %s: %s => %s", (x,y), (x_i, y_i), new_vert_idx) - bm.new_edge(vert_index, new_vert_idx) - - for eqn in infinite_lines: - intersections = bounds.line_intersection(eqn) - if len(intersections) == 2: - v1, v2 = intersections - new_vert_1_idx = bm.new_vert(tuple(v1)) - new_vert_2_idx = bm.new_vert(tuple(v2)) - bounding_verts.append(new_vert_1_idx) - bounding_verts.append(new_vert_2_idx) - bm.new_edge(new_vert_1_idx, new_vert_2_idx) - else: - self.error("unexpected number of intersections of infinite line %s with area bounds: %s", eqn, intersections) - - if self.draw_bounds: + if self.draw_hangs or self.draw_bounds: + other_vert = bm.verts[other_vert_idx] + if other_vert is not None: + x2, y2 = tuple(other_vert) + intersection = bounds.segment_intersection((x,y), (x2,y2)) + if intersection is not None: + intersection = tuple(intersection) + new_vert_idx = bm.new_vert(intersection) + bounding_verts.append(new_vert_idx) + #info("CLIP: Added point: %s => %s", (x_i, y_i), new_vert_idx) + bm.new_edge(other_vert_idx, new_vert_idx) + + # Diagram lines that go infinitely from one side of diagram to another + infinite_lines = [] + # Lines that start at the one vertex of the diagram and go to infinity + rays = defaultdict(list) + if self.draw_hangs or self.draw_bounds: + for line_index, i1, i2 in all_edges: + if i1 == -1 or i2 == -1: + line = lines[line_index] + a, b, c = line + eqn = LineEquation2D(a, b, -c) + if i1 == -1 and i2 != -1: + rays[i2].append(eqn) + elif i2 == -1 and i1 != -1: + rays[i1].append(eqn) + elif i1 == -1 and i2 == -1: + infinite_lines.append(eqn) + + for vert_index in rays.keys(): + x,y = bm.verts[vert_index] + vert = Vector((x,y)) + if vert_index not in verts_to_remove: + for line in rays[vert_index]: + intersection = bounds.ray_intersection(vert, line) + intersection = tuple(intersection) + new_vert_idx = bm.new_vert(intersection) + bounding_verts.append(new_vert_idx) + #info("INF: Added point: %s: %s => %s", (x,y), (x_i, y_i), new_vert_idx) + bm.new_edge(vert_index, new_vert_idx) + + for eqn in infinite_lines: + intersections = bounds.line_intersection(eqn) + if len(intersections) == 2: + v1, v2 = intersections + new_vert_1_idx = bm.new_vert(tuple(v1)) + new_vert_2_idx = bm.new_vert(tuple(v2)) + bounding_verts.append(new_vert_1_idx) + bounding_verts.append(new_vert_2_idx) + bm.new_edge(new_vert_1_idx, new_vert_2_idx) + else: + self.error("unexpected number of intersections of infinite line %s with area bounds: %s", eqn, intersections) + + if self.draw_bounds and bounding_verts: bounding_verts.sort(key = lambda idx: atan2(bm.verts[idx][1], bm.verts[idx][0])) for i, j in zip(bounding_verts, bounding_verts[1:]): bm.new_edge(i, j) -- GitLab From 1676133c0b7359851513c9955444e57f3caca5e4 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Oct 2019 23:23:27 +0500 Subject: [PATCH 07/11] Voronoi node documentation. --- docs/nodes/modifier_make/voronoi_2d.rst | 55 ++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/docs/nodes/modifier_make/voronoi_2d.rst b/docs/nodes/modifier_make/voronoi_2d.rst index 689e58a12..23fd20700 100644 --- a/docs/nodes/modifier_make/voronoi_2d.rst +++ b/docs/nodes/modifier_make/voronoi_2d.rst @@ -1,8 +1,53 @@ -Voronoi 2D and Delaunay -======================= +Voronoi 2D Node +=============== -This is a spartan note. Both these nodes are written in the same file so both online/offline help files will point into this file. +Functionality +------------- -Bugs? -The most recent mention of a bug for the Delaunay Node is that you must ensure that your input mesh has no doubles (co-location vertices). Else you will get an edge with zero distance for which the voronoi diagram will fail to compute (due to division by zero) +This node generates Voronoi_ diagram for the provided set of vertices in 2D space (in XOY plane). + +In general, Voronoi diagram is infinite construction that covers the whole XOY +plane. We cannot deal with such endless thing, so we have to clip that with +some bounds. It is possible to define bounds either based on bounding box of +provided vertices, or based on a circle that encloses all provided vertices. + +When we clip the diagram, there can be clipped polygons (they are produced when +the bounding line splits the polygon from original diagram in two parts) or +clipped lines (because some lines in original Voronoi diagram are endless). + +.. _Voronoi: https://en.wikipedia.org/wiki/Voronoi_diagram + +Inputs +------ + +The node has the following inputs: + +* **Vertices**. Set of input vertices to build Voronoi diagram for. + +Parameters +---------- + +This node has the following parameters: + +- **Bounds Mode**. The mode of diagram bounds definition. Possible values are + **Bounding Box** and **Circle**. The default value is **Bounding Box**. +- **Draw Bounds**. If checked, then the edges connecting boundary vertices will + be generated. Checked by default. +- **Draw Tails**. If checked, then the edges that go from the central part of + diagram to outside the bounding line, will be generated. This parameter is + available only if **Draw Bounds** is not checked. Checked by default. +- **Clipping**. Amount of space to be added for bounding line. If bounds are + defined by bounding box, then this amount of space will be added in each + direction (top, bottom, right and left). If bounds are defined by bounding + circle, then this amount will be added to the circle's radius. Default value is 1.0. + +Outputs +------- + +This node has the following outputs: + +- **Vertices** +- **Edges** + +It is possible to use **Fill Holes** node to generate polygons for the diagram. -- GitLab From 91d80bd20ec5a0ac256e85ab25ce3204b07722be Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Oct 2019 00:00:49 +0500 Subject: [PATCH 08/11] Fix in bounds definition. --- nodes/modifier_make/voronoi_2d.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index 55fcba1ce..fe156cef2 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -49,6 +49,9 @@ class Bounds(object): else: raise Exception("Unknown bounds type") + def __repr__(self): + return f"Bounds[C: {self.center}, R: {self.r_max}, X: {self.x_min} - {self.x_max}, Y: {self.y_min} - {self.y_max}]" + class Mesh2D(object): def __init__(self): self.verts = [] @@ -276,10 +279,10 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): for obj in points_in: bounds = Bounds.new(self.bound_mode) pt_list = [] - bounds.x_max = obj[0][0] - bounds.x_min = obj[0][0] - bounds.y_min = obj[0][1] - bounds.y_max = obj[0][1] + bounds.x_max = -BIG_FLOAT + bounds.x_min = BIG_FLOAT + bounds.y_min = BIG_FLOAT + bounds.y_max = -BIG_FLOAT x0, y0, z0 = center(obj) bounds.center = (x0, y0) # creates points in format for voronoi library, throwing away z @@ -289,7 +292,7 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): bounds.x_max = max(x, bounds.x_max) bounds.x_min = min(x, bounds.x_min) bounds.y_max = max(y, bounds.y_max) - bounds.y_min = min(x, bounds.x_min) + bounds.y_min = min(y, bounds.y_min) pt_list.append(Site(x, y)) delta = self.clip -- GitLab From b153c44575f47d18fea0cc6b5bf2ce7cef9775c4 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Oct 2019 19:46:27 +0500 Subject: [PATCH 09/11] Add examples to documentation [skip ci] --- docs/nodes/modifier_make/voronoi_2d.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/nodes/modifier_make/voronoi_2d.rst b/docs/nodes/modifier_make/voronoi_2d.rst index 23fd20700..c4569aad5 100644 --- a/docs/nodes/modifier_make/voronoi_2d.rst +++ b/docs/nodes/modifier_make/voronoi_2d.rst @@ -51,3 +51,16 @@ This node has the following outputs: It is possible to use **Fill Holes** node to generate polygons for the diagram. +Examples of usage +----------------- + +Simple example: + +.. image:: https://user-images.githubusercontent.com/284644/67318730-4a42d600-f525-11e9-88bf-1ec882cfdb2e.png + +Circular clipping: + +.. image:: https://user-images.githubusercontent.com/284644/67318729-4a42d600-f525-11e9-9909-2dce4218f89b.png + +.. image:: https://user-images.githubusercontent.com/284644/67318728-4a42d600-f525-11e9-961c-26f2f72e749a.png + -- GitLab From 636515724027fc53bc7a64db7effdd7f81783f28 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Oct 2019 20:31:54 +0500 Subject: [PATCH 10/11] More correct rays handling. and a code documentation. --- nodes/modifier_make/voronoi_2d.py | 45 ++++++++++++++++++++++++++----- utils/geom.py | 2 +- utils/voronoi.py | 21 ++++++--------- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index fe156cef2..2569913f6 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -149,8 +149,7 @@ class BoxBounds(Bounds): return nearest def ray_intersection(self, p, line): - if not isinstance(p, Vector): - p = Vector(p) + p = Vector(center(line.sites)) min_r = BIG_FLOAT nearest = None @@ -200,7 +199,9 @@ class CircleBounds(Bounds): return r[1] def ray_intersection(self, p, line): + p = Vector(center(line.sites)) intersection = self.circle.intersect_with_line(line) + #info("RI: {line} X {self.circle} => {intersection}") if intersection is None: return None else: @@ -217,7 +218,10 @@ class CircleBounds(Bounds): return intersection class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): - ''' vr Voronoi 2d line ''' + """ + Triggers: Voronoi vr + Tooltip: Generate 2D Voronoi diagram for a set of vertices. + """ bl_idname = 'Voronoi2DNode' bl_label = 'Voronoi 2D' bl_icon = 'OUTLINER_OB_EMPTY' @@ -278,7 +282,7 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): edges_out = [] for obj in points_in: bounds = Bounds.new(self.bound_mode) - pt_list = [] + source_sites = [] bounds.x_max = -BIG_FLOAT bounds.x_min = BIG_FLOAT bounds.y_min = BIG_FLOAT @@ -293,7 +297,7 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): bounds.x_min = min(x, bounds.x_min) bounds.y_max = max(y, bounds.y_max) bounds.y_min = min(y, bounds.y_min) - pt_list.append(Site(x, y)) + source_sites.append(Site(x, y)) delta = self.clip bounds.x_max = bounds.x_max + delta @@ -304,7 +308,11 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): bounds.r_max = bounds.r_max + delta - verts, lines, all_edges = computeVoronoiDiagram(pt_list) + voronoi_data = computeVoronoiDiagram(source_sites) + verts = voronoi_data.vertices + lines = voronoi_data.lines + all_edges = voronoi_data.edges + finite_edges = [(edge[1], edge[2]) for edge in all_edges if -1 not in edge] bm = Mesh2D.from_pydata(verts, finite_edges) @@ -313,6 +321,10 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): edges_to_remove = set() bounding_verts = [] + # For each diagram vertex that is outside of the bounds, + # cut each edge connected with that vertex by bounding line. + # Remove such vertices, remove such edges, and instead add + # vertices lying on the bounding line and corresponding edges. for vert_idx, vert in enumerate(bm.verts[:]): x, y = tuple(vert) if not bounds.contains((x,y)): @@ -336,18 +348,36 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): # Lines that start at the one vertex of the diagram and go to infinity rays = defaultdict(list) if self.draw_hangs or self.draw_bounds: + sites_by_line = defaultdict(list) + + for site_idx in voronoi_data.polygons.keys(): + for line_index, i1, i2 in voronoi_data.polygons[site_idx]: + if i1 == -1 or i2 == -1: + site = source_sites[site_idx] + sites_by_line[line_index].append((site.x, site.y)) + for line_index, i1, i2 in all_edges: if i1 == -1 or i2 == -1: line = lines[line_index] a, b, c = line eqn = LineEquation2D(a, b, -c) if i1 == -1 and i2 != -1: + eqn.sites = sites_by_line[line_index] rays[i2].append(eqn) elif i2 == -1 and i1 != -1: + eqn.sites = sites_by_line[line_index] rays[i1].append(eqn) elif i1 == -1 and i2 == -1: infinite_lines.append(eqn) + # For each (half-infinite) ray, calculate it's intersection + # with the bounding line and draw an edge from ray's beginning to + # the bounding line. + # NB: The data returned from voronoi.py for such lines + # is a vertex and a line equation. The line obviously intersects + # the bounding line in two points; which one should we choose? + # Let's choose that one which is closer to site points which the + # line is dividing. for vert_index in rays.keys(): x,y = bm.verts[vert_index] vert = Vector((x,y)) @@ -360,6 +390,9 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): #info("INF: Added point: %s: %s => %s", (x,y), (x_i, y_i), new_vert_idx) bm.new_edge(vert_index, new_vert_idx) + # For each infinite (in two directions) line, + # calculate two it's intersections with the bounding + # line and connect them by an edge. for eqn in infinite_lines: intersections = bounds.line_intersection(eqn) if len(intersections) == 2: diff --git a/utils/geom.py b/utils/geom.py index 1d57c1055..3b06dd7a5 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -1453,7 +1453,7 @@ class LineEquation2D(object): self.b = b self.c = c - def __str__(self): + def __repr__(self): return f"{self.a}*x + {self.b}*y + {self.c} = 0" @classmethod diff --git a/utils/voronoi.py b/utils/voronoi.py index 0d66cc8a3..8e73f79a1 100644 --- a/utils/voronoi.py +++ b/utils/voronoi.py @@ -769,29 +769,24 @@ class SiteList(object): #------------------------------------------------------------------ def computeVoronoiDiagram(points): """ Takes a list of point objects (which must have x and y fields). - Returns a 3-tuple of: + Returns a Context object. - (1) a list of 2-tuples, which are the x,y coordinates of the - Voronoi diagram vertices - (2) a list of 3-tuples (a,b,c) which are the equations of the - lines in the Voronoi diagram: a*x + b*y = c - (3) a list of 3-tuples, (l, v1, v2) representing edges of the + (1) context.vertices: a list of 2-tuples, which are the x,y + coordinates of the Voronoi diagram vertices + (2) context.lines: a list of 3-tuples (a,b,c) which are the + equations of the lines in the Voronoi diagram: a*x + b*y = c + (3) context.edges: a list of 3-tuples, (l, v1, v2) representing edges of the Voronoi diagram. l is the index of the line, v1 and v2 are the indices of the vetices at the end of the edge. If v1 or v2 is -1, the line extends to infinity. + (4) context.polygons: a dict of site:[edges] pairs """ -# siteList = SiteList(points) -# context = Context() -# voronoi(siteList,context) -# return (context.vertices,context.lines,context.edges) - - siteList = SiteList(points) context = Context() context.triangulate = True voronoi(siteList,context) - return (context.vertices,context.lines,context.edges) + return context #------------------------------------------------------------------ def computeDelaunayTriangulation(points): -- GitLab From e565c1b7ecf393f049db4069bd9a31a7294e4def Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Oct 2019 20:36:57 +0500 Subject: [PATCH 11/11] Add a todo. [skip ci]. --- nodes/modifier_make/voronoi_2d.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nodes/modifier_make/voronoi_2d.py b/nodes/modifier_make/voronoi_2d.py index 2569913f6..5f3e2bbdf 100644 --- a/nodes/modifier_make/voronoi_2d.py +++ b/nodes/modifier_make/voronoi_2d.py @@ -405,6 +405,14 @@ class Voronoi2DNode(bpy.types.Node, SverchCustomTreeNode): else: self.error("unexpected number of intersections of infinite line %s with area bounds: %s", eqn, intersections) + # TODO: there could be (finite) edges, which have both ends + # outside of the bounding line. We could detect such edges and + # process similarly to infinite lines - calculate two intersections + # with the bounding line and connect them by an edge. + # Currently I consider such cases as rare, so this is a low priority issue. + # Btw, such edges do not fall under definition of either "bounding edge" + # or "hanging edge"; so should we add a separate checkbox for such edges?... + if self.draw_bounds and bounding_verts: bounding_verts.sort(key = lambda idx: atan2(bm.verts[idx][1], bm.verts[idx][0])) for i, j in zip(bounding_verts, bounding_verts[1:]): -- GitLab