From 2f5de9090c8d9400997e3f3e9d14823dafa5c686 Mon Sep 17 00:00:00 2001 From: satabol Date: Tue, 8 Oct 2024 16:29:24 +0300 Subject: [PATCH 1/7] v1 --- index.yaml | 3 + nodes/CAD/straight_skeleton_2d_test001.py | 655 ++++++++++++++++++++++ nodes/CAD/stright_skeleton_2d.py | 500 +++++++++++++++++ 3 files changed, 1158 insertions(+) create mode 100644 nodes/CAD/straight_skeleton_2d_test001.py create mode 100644 nodes/CAD/stright_skeleton_2d.py diff --git a/index.yaml b/index.yaml index 23fbade3ab..fa533382d6 100644 --- a/index.yaml +++ b/index.yaml @@ -493,6 +493,9 @@ - SvCropMesh2D - SvCSGBooleanNodeMK2 - SvWafelNode + - --- + - SvStraightSkeleton2D + - SvStraightSkeleton2DTest001 - --- diff --git a/nodes/CAD/straight_skeleton_2d_test001.py b/nodes/CAD/straight_skeleton_2d_test001.py new file mode 100644 index 0000000000..bf644a3ec9 --- /dev/null +++ b/nodes/CAD/straight_skeleton_2d_test001.py @@ -0,0 +1,655 @@ +# 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 bpy + +import bpy, math, bmesh +from mathutils import Vector, Matrix +from collections import namedtuple + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode +from sverchok.utils.geom_2d.merge_mesh import merge_mesh +from sverchok.utils.nodes_mixins.sockets_config import ModifierLiteNode +from sverchok.data_structure import dataCorrect, updateNode, zip_long_repeat, ensure_nesting_level, flatten_data + +## internal.py ######################################### +Plane = namedtuple('Plane', 'normal distance') + +def normalOfPolygon(vertices): + normal = Vector((0.0, 0.0, 0.0)) + for index, current in enumerate(vertices): + prev = vertices[index-1] + normal += (prev-vertices[0]).cross(current-vertices[0]) + return normal + +def areaOfPolygon(vertices): + return normalOfPolygon(vertices).length*0.5 + +def linePlaneIntersection(origin, dir, plane): + # return mathutils.geometry.intersect_line_plane(origin, origin+dir, plane.normal*plane.distance, plane.normal) + det = dir@plane.normal + return float('nan') if det == 0 else (plane.distance-origin@plane.normal)/det + +def planePlaneIntersection(planeA, planeB, tollerance=0.0001): + # return mathutils.geometry.intersect_plane_plane(planeA.normal*planeA.distance, planeA.normal, planeB.normal*planeB.distance, planeB.normal) + if 1.0-abs(planeA.normal@planeB.normal) < tollerance: + return ('Parallel' if abs(planeA.distance-planeB.distance) > tollerance else 'Coplanar', None, None) + dir = planeA.normal.cross(planeB.normal).normalized() + ray_origin = planeA.normal*planeA.distance + ray_dir = planeA.normal.cross(dir) + origin = ray_origin+ray_dir*linePlaneIntersection(ray_origin, ray_dir, planeB) + return ('Intersecting', origin, dir) + +def linePointDistance(begin, dir, point): + return (point-begin).cross(dir.normalized()).length + +def nearestPointOfLines(originA, dirA, originB, dirB, param_tollerance=0.0, dist_tollerance=0.001): + # https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points + normal = dirA.cross(dirB) + normalA = dirA.cross(normal) + normalB = dirB.cross(normal) + divisorA = dirA@normalB + divisorB = dirB@normalA + originAB = originB-originA + if abs(divisorA) <= param_tollerance or abs(divisorB) <= param_tollerance: + if dirA@dirA == 0.0 or dirB@dirB == 0.0 or linePointDistance(originA, dirA, originB) >= dist_tollerance: + return ('Parallel', float('nan'), float('nan')) + paramA = originAB@dirA/(dirA@dirA) + paramB = -originAB@dirB/(dirB@dirB) + return ('Coaxial', paramA, paramB) + else: + paramA = originAB@normalB/divisorA + paramB = -originAB@normalA/divisorB + nearestPointA = originA+dirA*paramA + nearestPointB = originB+dirB*paramB + return ('Crossing' if (nearestPointA-nearestPointB).length <= dist_tollerance else 'Skew', paramA, paramB) + +def lineSegmentLineSegmentIntersection(lineAVertexA, lineAVertexB, lineBVertexA, lineBVertexB): + dirA = lineAVertexB-lineAVertexA + dirB = lineBVertexB-lineBVertexA + type, paramA, paramB = nearestPointOfLines(lineAVertexA, dirA, lineBVertexA, dirB) + if type == 'Parallel' or type == 'Skew': + return (float('nan'), float('nan')) + if type == 'Coaxial': + if paramA < 0.0 and paramB < 0.0: # Facing away from one another + return (float('nan'), float('nan')) + if paramA > 0.0 and paramB > 0.0: # Facing towards each other + if paramA > 1.0 and (lineBVertexB-lineAVertexA)@dirA > 1.0: # End of B is not in A + return (float('nan'), float('nan')) + elif paramA > 1.0 or paramB > 1.0: # One is chasing the other but out of reach + return (float('nan'), float('nan')) + paramA = max(0.0, (lineBVertexB-lineAVertexA)@dirA/(dirA@dirA)) + paramB = max(0.0, (lineAVertexB-lineBVertexA)@dirB/(dirB@dirB)) + return (paramA, paramB) + if paramA < 0.0 or paramA > 1.0 or paramB < 0.0 or paramB > 1.0: # Intersection is outside the line segments + return (float('nan'), float('nan')) + return (paramA, paramB) + +def rayLineSegmentIntersection(originA, dirA, lineVertexA, lineVertexB): + dirB = lineVertexB-lineVertexA + type, paramA, paramB = nearestPointOfLines(originA, dirA, lineVertexA, dirB) + if type == 'Parallel' or type == 'Skew': + return float('nan') + if type == 'Coaxial': + if paramA > 0.0: + return paramA if (paramB < 0.0) else max(0.0, (lineVertexB-originA)@dirA/(dirA@dirA)) + else: + return float('nan') if (paramB < 0.0 or paramB > 1.0) else 0.0 + if paramA < 0.0 or paramB < 0.0 or paramB > 1.0: # Intersection is behind the rays origin or outside of the line segment + return float('nan') + return paramA + +def rayRayIntersection(originA, dirA, originB, dirB): + type, paramA, paramB = nearestPointOfLines(originA, dirA, originB, dirB) + if type == 'Parallel' or type == 'Skew': + return (float('nan'), float('nan')) + if type == 'Coaxial': + if paramA < 0.0 and paramB < 0.0: # Facing away from one another + return (float('nan'), float('nan')) + if paramA > 0.0 and paramB > 0.0: # Facing towards each other + paramSum = paramA+paramB + paramA = paramA*paramA/paramSum + paramB = paramB*paramB/paramSum + return (paramA, paramB) + return (paramA, 0.0) if paramA > 0.0 else (0.0, paramB) # One is chasing the other + if paramA < 0.0 or paramB < 0.0: # Intersection is behind the rays origins + return (float('nan'), float('nan')) + return (paramA, paramB) + +def insort_right(sorted_list, keyfunc, entry, lo=0, hi=None): + if hi == None: + hi = len(sorted_list) + while lo < hi: + mid = (lo+hi)//2 + if keyfunc(entry) < keyfunc(sorted_list[mid]): + hi = mid + else: + lo = mid+1 + sorted_list.insert(lo, entry) + + + +def selectedPolygons(src_obj): + polygons = [] + in_edit_mode = (src_obj.mode == 'EDIT') + bpy.ops.object.mode_set(mode='OBJECT') + if src_obj.type == 'CURVE': + if in_edit_mode: + splines = [] + for spline in bpy.context.object.data.splines: + selected = True + if spline.type == 'POLY': + for index, point in enumerate(spline.points): + if point.select == False: + selected = False + break + if selected: + splines.append(spline) + else: + splines = src_obj.data.splines + for spline in splines: + polygons.append(list(point.co.xyz for point in spline.points)) + else: + loops = [] + for face in src_obj.data.polygons: + if in_edit_mode and not face.select: + continue + polygons.append(list(src_obj.data.vertices[vertex_index].co for vertex_index in face.vertices)) + return polygons + +def addObject(type, name): + if type == 'CURVE': + data = bpy.data.curves.new(name=name, type='CURVE') + data.dimensions = '3D' + elif type == 'MESH': + data = bpy.data.meshes.new(name=name) + obj = bpy.data.objects.new(name, data) + obj.location = bpy.context.scene.cursor.location + bpy.context.scene.collection.objects.link(obj) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + return obj + +def addPolygonSpline(obj, cyclic, vertices, weights=None, select=False): + spline = obj.data.splines.new(type='POLY') + spline.use_cyclic_u = cyclic + spline.points.add(len(vertices)-1) + for index, point in enumerate(spline.points): + point.co.xyz = vertices[index] + point.select = select + if weights: + point.weight_softbody = weights[index] + return spline + + + +class SlabIntersection: + __slots__ = ['prev_slab', 'next_slab', 'origin', 'dir', 'begin_param', 'end_param'] + def __init__(self, prev_slab, next_slab, origin, dir, begin_param, end_param): + self.prev_slab = prev_slab + self.next_slab = next_slab + self.origin = origin + self.dir = dir + self.begin_param = begin_param + self.end_param = end_param + + def reverse(self): + self.dir *= -1.0 + [self.begin_param, self.end_param] = [-self.end_param, -self.begin_param] + + def otherSlab(self, slab): + return self.prev_slab if slab == self.next_slab else self.next_slab + +class Slab: + __slots__ = ['edge', 'slope', 'plane', 'prev_slab', 'next_slab', 'prev_polygon_vertex', 'next_polygon_vertex', 'prev_lightcycles', 'next_lightcycles', 'vertices', 'slab_intersections'] + def __init__(self, polygon_normal, prev_polygon_vertex, next_polygon_vertex): + self.edge = (next_polygon_vertex-prev_polygon_vertex).normalized() + edge_orthogonal = self.edge.cross(polygon_normal).normalized() + normal = (polygon_normal+edge_orthogonal).normalized() + self.slope = (polygon_normal-edge_orthogonal).normalized() + self.plane = Plane(normal=normal, distance=next_polygon_vertex@normal) + self.prev_lightcycles = [] + self.next_lightcycles = [] + self.prev_polygon_vertex = prev_polygon_vertex + self.next_polygon_vertex = next_polygon_vertex + self.vertices = [self.prev_polygon_vertex, self.next_polygon_vertex] + self.slab_intersections = [] + + def isOuterOfCollision(self, in_dir, out_dir, polygon_normal): + normal = in_dir.cross(polygon_normal) + return (normal@self.plane.normal > 0.0) == (normal@out_dir > 0.0) + + def calculateVerticesFromLightcycles(self): + def handleSide(lightcycles, prepend): + for lightcycle in lightcycles: + if lightcycle.slab_intersection.origin is None or lightcycle.slab_intersection.dir is None or lightcycle.slab_intersection.end_param is None: + pass + else: + vertex = lightcycle.slab_intersection.origin+lightcycle.slab_intersection.dir*lightcycle.slab_intersection.end_param + if prepend: + self.vertices.insert(0, vertex) + else: + self.vertices.append(vertex) + handleSide(self.prev_lightcycles, True) + handleSide(self.next_lightcycles, False) + + def rayBoundaryIntersection(self, origin, dir, tollerance=0.0001): + intersections = [] + for i in range(0, len(self.vertices)+1): + is_last = (i == 0 or i == len(self.vertices)) + type, paramA, paramB = nearestPointOfLines(origin, dir, self.vertices[0 if i == 0 else i-1], self.slope if is_last else self.vertices[i]-self.vertices[i-1]) + if type == 'Crossing': + if paramB > -tollerance and (is_last or paramB < 1.0+tollerance): + intersections.append((i, paramA)) + elif type == 'Coaxial': + assert(not is_last) + intersections.append((i-1, paramA)) + intersections.append((i, (self.vertices[i]-origin)@dir/(dir@dir))) + intersections.sort(key=lambda entry: entry[1]) + i = 1 + while i < len(intersections): + if intersections[i][1]-intersections[i-1][1] < tollerance: + del intersections[i] + else: + i += 1 + return intersections + + def calculateSlabIntersection(self, other_slab, is_first, tollerance=0.0001): + lightcycles = self.next_lightcycles if is_first else self.prev_lightcycles + if len(lightcycles) > 0 and (lightcycles[0].slab_intersection.prev_slab == other_slab or lightcycles[0].slab_intersection.next_slab == other_slab): + return + type, origin, dir = planePlaneIntersection(self.plane, other_slab.plane) + if type != 'Intersecting': + if self.prev_slab == other_slab or self.next_slab == other_slab: + slab_intersection = SlabIntersection(self, other_slab, self.prev_polygon_vertex if self.prev_slab == other_slab else self.next_polygon_vertex, self.slope, 0.0, float('inf')) + self.slab_intersections.append(slab_intersection) + other_slab.slab_intersections.append(slab_intersection) + return + intersectionsA = self.rayBoundaryIntersection(origin, dir) + intersectionsB = other_slab.rayBoundaryIntersection(origin, dir) + if len(intersectionsA) == 2 and len(intersectionsB) == 2: + begin_param = max(intersectionsA[0][1], intersectionsB[0][1]) + end_param = min(intersectionsA[1][1], intersectionsB[1][1]) + if begin_param < end_param and end_param-begin_param >= tollerance: + slab_intersection = SlabIntersection(self, other_slab, origin+begin_param*dir, dir, 0.0, end_param-begin_param) + self.slab_intersections.append(slab_intersection) + other_slab.slab_intersections.append(slab_intersection) + + def calculateVerticesFromIntersections(self, tollerance=0.001): + pivot = self.prev_polygon_vertex + current_line = None + for candidate in self.slab_intersections: + if candidate.prev_slab == self.prev_slab or candidate.next_slab == self.prev_slab: + current_line = candidate + break + if current_line == None: + print('ERROR: calculateVerticesFromIntersections() could not find the first current_line') + return + if abs((current_line.origin+current_line.dir*current_line.begin_param-pivot)@current_line.dir) > abs((current_line.origin+current_line.dir*current_line.end_param-pivot)@current_line.dir): + current_line.reverse() + self.vertices = [self.prev_polygon_vertex, self.next_polygon_vertex] + while current_line.prev_slab != self.next_slab and current_line.next_slab != self.next_slab: + self.slab_intersections.remove(current_line) + pivot_param = (pivot-current_line.origin)@current_line.dir + best_candidate = None + best_param = float('nan') + current_other_slab = current_line.otherSlab(self) + lightcycles = [] + if len(current_other_slab.prev_lightcycles) > 0: + lightcycles.append(current_other_slab.prev_lightcycles[-1]) + if len(current_other_slab.next_lightcycles) > 0: + lightcycles.append(current_other_slab.next_lightcycles[-1]) + for lightcycle in lightcycles: + if lightcycle.slab_intersection.origin is None or lightcycle.slab_intersection.dir is None: + pass + else: + param = linePlaneIntersection(lightcycle.slab_intersection.origin, lightcycle.slab_intersection.dir, self.plane) + if lightcycle.slab_intersection.begin_param-tollerance <= param and param <= lightcycle.slab_intersection.end_param+tollerance: + candidate_other_slab = lightcycle.slab_intersection.otherSlab(current_other_slab) + position = lightcycle.slab_intersection.origin+lightcycle.slab_intersection.dir*param + param = (position-pivot)@current_line.dir + if candidate_other_slab != self and param > 0.0: + for candidate in self.slab_intersections: + if candidate.otherSlab(self) == candidate_other_slab: + best_candidate = candidate + best_param = current_line.end_param + if abs((best_candidate.origin+best_candidate.dir*best_candidate.begin_param-pivot)@best_candidate.dir) > abs((best_candidate.origin+best_candidate.dir*best_candidate.end_param-pivot)@best_candidate.dir): + best_candidate.reverse() + break + for candidate in self.slab_intersections: + if candidate == best_candidate: + continue + type, paramA, paramB = nearestPointOfLines(current_line.origin, current_line.dir, candidate.origin, candidate.dir) + if (type == 'Crossing' or type == 'Coaxial') and pivot_param-tollerance <= paramA and \ + current_line.begin_param-tollerance <= paramA and paramA <= current_line.end_param+tollerance and \ + candidate.begin_param-tollerance <= paramB and paramB <= candidate.end_param+tollerance and \ + (best_candidate == None or best_param > paramA): + best_candidate = candidate + best_param = paramA + normal = self.plane.normal.cross(current_line.dir) + if (best_candidate.origin+best_candidate.dir*best_candidate.begin_param-pivot)@normal < (best_candidate.origin+best_candidate.dir*best_candidate.end_param-pivot)@normal: + best_candidate.reverse() + if best_candidate == None: + print('ERROR: calculateVerticesFromIntersections() could not find the next current_line') + return + pivot = current_line.origin+current_line.dir*best_param + current_line = best_candidate + self.vertices.insert(0, pivot) + self.slab_intersections = None + +class Collision: + __slots__ = ['winner_time', 'looser_time', 'winner', 'loosers', 'children'] + def __init__(self, winner_time, looser_time, winner, loosers): + self.winner_time = winner_time + self.looser_time = looser_time + self.winner = winner + self.loosers = loosers + self.children = [] + + def checkCandidate(self): + if self.winner != None and self.winner.collision != None and self.winner.collision.looser_time < self.winner_time: + return False + for looser in self.loosers: + if looser.collision != None: + return False + return True + + def collide(self, lightcycles, collision_candidates, polygon_vertices, polygon_normal, tollerance=0.0001): + for looser in self.loosers: + looser.collision = self + if len(self.loosers) == 2: + assert(self.loosers[0].ground_normal@self.loosers[1].ground_normal > 0.0) + position = self.loosers[0].ground_origin+self.loosers[0].ground_velocity*self.looser_time + dirA = self.loosers[0].ground_velocity.normalized() + dirB = self.loosers[1].ground_velocity.normalized() + ground_dir = dirA+dirB + if ground_dir.length > tollerance: + index = 1 if self.loosers[0].slab_intersection.prev_slab.isOuterOfCollision(dirA, ground_dir, polygon_normal) else 0 + if dirA.cross(dirB)@polygon_normal > 0.0: + index = 1-index + self.children = [Lightcycle( + lightcycles, collision_candidates, polygon_vertices, polygon_normal, False, + self.looser_time, self.loosers[index].slab_intersection.prev_slab, self.loosers[1-index].slab_intersection.next_slab, + position, ground_dir.normalized(), self.loosers[0].ground_normal + )] + else: + ground_dir = dirA.cross(self.loosers[0].ground_normal) + index = 1 if self.loosers[0].slab_intersection.prev_slab.isOuterOfCollision(dirA, ground_dir, polygon_normal) else 0 + self.children = [Lightcycle( + lightcycles, collision_candidates, polygon_vertices, polygon_normal, False, + self.looser_time, self.loosers[index].slab_intersection.prev_slab, self.loosers[1-index].slab_intersection.next_slab, + position, ground_dir, self.loosers[0].ground_normal + ), Lightcycle( + lightcycles, collision_candidates, polygon_vertices, polygon_normal, True, + self.looser_time, self.loosers[1-index].slab_intersection.prev_slab, self.loosers[index].slab_intersection.next_slab, + position, -ground_dir, self.loosers[0].ground_normal + )] + +class Lightcycle: + __slots__ = ['start_time', 'ground_origin', 'ground_velocity', 'ground_normal', 'inwards', 'collision', 'slab_intersection'] + def __init__(self, lightcycles, collision_candidates, polygon_vertices, polygon_normal, immunity, start_time, prev_slab, next_slab, position, ground_dir, ground_normal): + exterior_angle = math.pi-math.acos(max(-1.0, min(prev_slab.edge@-next_slab.edge, 1.0))) + # pitch_angle = math.atan(math.cos(exterior_angle*0.5)) + ground_speed = 1.0/math.cos(exterior_angle*0.5) + self.start_time = start_time + self.ground_origin = position + self.ground_velocity = ground_dir*ground_speed + self.ground_normal = ground_normal + self.inwards = (self.ground_normal@polygon_normal > 0.0) + self.collision = None + self.slab_intersection = SlabIntersection(prev_slab, next_slab, None, None, 0.0, 0.0) + if self.inwards: + prev_slab.next_lightcycles.append(self) + next_slab.prev_lightcycles.append(self) + self.collideWithLightcycles(lightcycles, collision_candidates, immunity) + self.collideWithPolygon(collision_candidates, polygon_vertices, immunity) + lightcycles.append(self) + + def collideWithLightcycles(self, lightcycles, collision_candidates, immunity, arrival_tollerance=0.001): + for i in range(0, len(lightcycles)-1 if immunity == True else len(lightcycles)): + timeA, timeB = rayRayIntersection(self.ground_origin, self.ground_velocity, lightcycles[i].ground_origin, lightcycles[i].ground_velocity) + if math.isnan(timeA) or math.isnan(timeB): + continue + timeA += self.start_time + timeB += lightcycles[i].start_time + winner = None if abs(timeA-timeB) < arrival_tollerance else self if timeA < timeB else lightcycles[i] + # TODO: Insert in manyfold collision + insort_right(collision_candidates, lambda collision: collision.looser_time, Collision( + winner_time=min(timeA, timeB), + looser_time=max(timeA, timeB), + winner=winner, + loosers=([self, lightcycles[i]] if winner == None else [self if timeA > timeB else lightcycles[i]]) + )) + + def collideWithPolygon(self, collision_candidates, polygon_vertices, immunity): + min_time = float('inf') + for index in range(0, len(polygon_vertices)): + if type(immunity) is int and (index == immunity or index == (immunity+1)%len(polygon_vertices)): + continue + time = rayLineSegmentIntersection(self.ground_origin, self.ground_velocity, polygon_vertices[index-1], polygon_vertices[index]) + if not math.isnan(time): + min_time = min(time+self.start_time, min_time) + if min_time < float('inf'): + insort_right(collision_candidates, lambda collision: collision.looser_time, Collision( + winner_time=0.0, + looser_time=min_time, + winner=None, + loosers=[self] + )) + + def calculateSlabIntersection(self, tollerance=0.0001): + if self.collision == None: + return + self.slab_intersection.origin = self.ground_origin+self.ground_normal*self.start_time + dir = self.ground_velocity+self.ground_normal + self.slab_intersection.dir = dir.normalized() + self.slab_intersection.end_param = dir@self.slab_intersection.dir*(self.collision.looser_time-self.start_time) + if self.inwards: + self.slab_intersection.prev_slab.slab_intersections.append(self.slab_intersection) + self.slab_intersection.next_slab.slab_intersections.append(self.slab_intersection) + +def straightSkeletonOfPolygon(polygon_vertices, mesh_data, height=1.5, tollerance=0.0001): + polygon_normal = normalOfPolygon(polygon_vertices).normalized() + polygon_plane = Plane(normal=polygon_normal, distance=polygon_vertices[0]@polygon_normal) + for polygon_vertex in polygon_vertices: + if abs(polygon_vertex@polygon_plane.normal-polygon_plane.distance) > tollerance: + return 'Polygon is not planar / level' + + polygon_tangent = (polygon_vertices[1]-polygon_vertices[0]).normalized() + plane_matrix = Matrix.Identity(4) + plane_matrix.col[0] = polygon_tangent.to_4d() + plane_matrix.col[1] = polygon_normal.cross(polygon_tangent).normalized().to_4d() + plane_matrix.col[2] = polygon_normal.to_4d() + plane_matrix.col[3] = (polygon_plane.normal*polygon_plane.distance).to_4d() + plane_matrix.col[0].w = plane_matrix.col[1].w = plane_matrix.col[2].w = 0.0 + plane_matrix_inverse = plane_matrix.inverted() + plane_matrix_inverse.row[2].zero() + polygon_vertices = [plane_matrix_inverse@vertex for vertex in polygon_vertices] + polygon_normal = Vector((0.0, 0.0, 1.0)) + + slabs = [] + lightcycles = [] + collision_candidates = [] + + for index, next_polygon_vertex in enumerate(polygon_vertices): + prev_polygon_vertex = polygon_vertices[index-1] + slabs.append(Slab(polygon_normal, prev_polygon_vertex, next_polygon_vertex)) + + for index, prev_slab in enumerate(slabs): + next_slab = slabs[(index+1)%len(polygon_vertices)] + next_slab.prev_slab = prev_slab + prev_slab.next_slab = next_slab + Lightcycle( + lightcycles, collision_candidates, polygon_vertices, polygon_normal, index, + 0.0, prev_slab, next_slab, polygon_vertices[index], + (prev_slab.edge-next_slab.edge).normalized(), prev_slab.edge.cross(-next_slab.edge).normalized() + ) + + i = 0 + while i < len(collision_candidates): + collision = collision_candidates[i] + if collision.checkCandidate(): + collision.collide(lightcycles, collision_candidates, polygon_vertices, polygon_normal) + if len(collision.loosers) > 2: + return 'Manyfold collision' # TODO + i += 1 + + verts = [] + edges = [] + faces = [] + + for lightcycle in lightcycles: + lightcycle.calculateSlabIntersection() + # if lightcycle.collision != None: + # verts += [lightcycle.slab_intersection.origin, lightcycle.slab_intersection.origin+lightcycle.slab_intersection.dir*lightcycle.slab_intersection.end_param] + # edges.append((len(verts)-2, len(verts)-1)) + + for j, slabA in enumerate(slabs): + slabA.calculateVerticesFromLightcycles() + for i, slabB in enumerate(slabs): + if i >= j: + continue + slabA.calculateSlabIntersection(slabB, i == 0) + # for slab_intersection in slabA.slab_intersections: + # verts += [slab_intersection.origin+slab_intersection.dir*slab_intersection.begin_param, slab_intersection.origin+slab_intersection.dir*slab_intersection.end_param] + # edges.append((len(verts)-2, len(verts)-1)) + + for index, slab in enumerate(slabs): + slab.calculateVerticesFromIntersections() + vert_index = len(verts) + verts += slab.vertices + faces.append(range(vert_index, len(verts))) + + #mesh_data.from_pydata(verts, edges, faces) + return plane_matrix, verts, edges, faces + + + +def sliceMesh(src_mesh, dst_obj, distances, axis): + if dst_obj.type == 'MESH': + dst_obj.data.clear_geometry() + else: + dst_obj.data.splines.clear() + out_vertices = [] + out_edges = [] + for distance in distances: + aux_mesh = src_mesh.copy() + cut_geometry = bmesh.ops.bisect_plane(aux_mesh, geom=aux_mesh.edges[:]+aux_mesh.faces[:], dist=0, plane_co=axis*distance, plane_no=axis, clear_outer=False, clear_inner=False)['geom_cut'] + edge_pool = set((e for e in cut_geometry if isinstance(e, bmesh.types.BMEdge))) + while len(edge_pool) > 0: + current_edge = edge_pool.pop() + first_vertex = current_vertex = current_edge.verts[0] + vertices = [current_vertex.co] + while True: + current_vertex = current_edge.other_vert(current_vertex) + if current_vertex == first_vertex: + break + vertices.append(current_vertex.co) + follow_edge_loop = False + for edge in current_vertex.link_edges: + if edge in edge_pool: + current_edge = edge + edge_pool.remove(current_edge) + follow_edge_loop = True + break + if not follow_edge_loop: + break + if dst_obj.type == 'MESH': + for i in range(len(out_vertices), len(out_vertices)+len(vertices)-1): + out_edges.append((i, i+1)) + if current_vertex == first_vertex: + out_edges.append((len(out_vertices), len(out_vertices)+len(vertices)-1)) + out_vertices += [Vector(vertex) for vertex in vertices] + else: + addPolygonSpline(dst_obj, current_vertex == first_vertex, vertices) + aux_mesh.free() + if dst_obj.type == 'MESH': + dst_obj.data.from_pydata(out_vertices, out_edges, []) + +## /internal.py ######################################### + +class SvStraightSkeleton2DTest001(ModifierLiteNode, SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Merge two 2d meshes + + Each mesh can have disjoint parts + Only X and Y coordinate takes in account + """ + bl_idname = 'SvStraightSkeleton2DTest001' + bl_label = 'Straight Skeleton 2D Test001' + bl_icon = 'AUTOMERGE_ON' + + def draw_buttons_ext(self, context, layout): + col = layout.column(align=True) + pass + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', 'vertices') + self.inputs.new('SvStringsSocket' , 'edges') + self.inputs.new('SvStringsSocket' , 'polygons') + #self.inputs.new('SvMatrixSocket' , "matrixes") + + self.inputs['vertices'].label = 'Vertices' + self.inputs['edges'].label = 'Edges' + self.inputs['polygons'].label = 'Polygons' + #self.inputs['matrixes'].label = 'Matrixes' + + self.outputs.new('SvVerticesSocket', 'vertices') + self.outputs.new('SvStringsSocket' , 'edges') + self.outputs.new('SvStringsSocket' , 'polygons') + + self.outputs['vertices'].label = 'Vertices' + self.outputs['edges'].label = 'Edges' + self.outputs['polygons'].label = 'Polygons' + + def process(self): + if not all([sock.is_linked for sock in self.inputs]): + return + if not any([sock.is_linked for sock in self.outputs]): + return + + inputs = self.inputs + _Vertices = inputs['vertices'].sv_get(default=[[]], deepcopy=False) + Vertices = ensure_nesting_level(_Vertices, 3) + _Edges = inputs['edges'].sv_get(default=[[]], deepcopy=False) + Edges = ensure_nesting_level(_Edges, 3) + _Faces = inputs['polygons'].sv_get(default=[[]], deepcopy=False) + Faces = ensure_nesting_level(_Faces, 3) + + res_verts = [] + res_edges = [] + res_faces = [] + + for verts_i, edges_i, faces_i in zip_long_repeat(Vertices, Edges, Faces): + + #src_obj = bpy.context.object + vverts_i = [Vector(v) for v in verts_i] + polygons = [vverts_i] #selectedPolygons(src_obj) + if len(faces_i) != 1: + self.report({'WARNING'}, 'Invalid selection') + return {'CANCELLED'} + #dst_obj = addObject('MESH', 'Straight Skeleton') + plane_matrix, verts, edges, faces = straightSkeletonOfPolygon(polygons[0], None) + if isinstance(plane_matrix, str): + self.report({'WARNING'}, result) + return {'CANCELLED'} + #dst_obj.matrix_world = src_obj.matrix_world@plane_matrix + res_verts.append(verts) + res_edges.append(edges) + res_faces.append(faces) + pass + + self.outputs['vertices'].sv_set(res_verts) + self.outputs['edges'].sv_set(res_edges) + self.outputs['polygons'].sv_set(res_faces) + + pass + +classes = [SvStraightSkeleton2DTest001,] +register, unregister = bpy.utils.register_classes_factory(classes) \ No newline at end of file diff --git a/nodes/CAD/stright_skeleton_2d.py b/nodes/CAD/stright_skeleton_2d.py new file mode 100644 index 0000000000..b08cee7dcf --- /dev/null +++ b/nodes/CAD/stright_skeleton_2d.py @@ -0,0 +1,500 @@ +# 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 bpy +from concurrent.futures import ThreadPoolExecutor + +import collections +import itertools +import numpy as np +import bpy, math, bmesh +from bpy.props import FloatProperty, BoolProperty, IntProperty +from mathutils import Vector, Matrix +from more_itertools import sort_together + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode +from sverchok.utils.geom_2d.merge_mesh import merge_mesh +from sverchok.utils.nodes_mixins.sockets_config import ModifierLiteNode +from sverchok.data_structure import dataCorrect, updateNode, zip_long_repeat, ensure_nesting_level, flatten_data +#from sverchok.utils.mesh.separate_loose_mesh import separate_loose_mesh +from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata +from sverchok.nodes.analyzer.mesh_filter import Edges +from sverchok.nodes.vector.vertices_sort import sort_vertices_by_connexions +from sverchok.utils.modules.polygon_utils import areas_from_polygons +from sverchok.utils.sv_operator_mixins import SvGenericNodeLocator + +from pySVCGAL.pySVCGAL import pySVCGAL_extrude_skeleton + +def vertices_sort_by_edges(verts_in, edges_in): + edges_indexes = list(itertools.chain(*edges_in)) + verts_out = [] + if len(edges_indexes)==0: + pass + else: + edges_indexes_0 = edges_indexes[ ::2] + edges_indexes_1 = edges_indexes[1::2] + + chain = [] + pos = 0 + v0_idx = edges_indexes_0[pos] + chain.append(v0_idx) + + while True: + v1_idx = edges_indexes_1[pos] + if v1_idx in chain: + # цикл прервать, кольцо + break + chain.append(v1_idx) + if v1_idx in edges_indexes_0: + pos = edges_indexes_0.index(v1_idx) + #v0_idx = edges_indexes_0[pos] + else: + # конец цепочки + break + + # Попробовать построить цепочку в обратном направлении (тут не в курсе, вышли из-за кольца + # или что достигнут конец цепочки: + + v1_idx = chain[0] + if v1_idx not in edges_indexes_1: + pass + else: + pos = edges_indexes_1.index( v1_idx ) + while True: + v0_idx = edges_indexes_0[pos] + if v0_idx in chain: + # цикл прервать, кольцо + break + chain.append(v0_idx) + if v0_idx in edges_indexes_1: + pos = edges_indexes_1.index(v0_idx) + #v1_idx = edges_indexes_1[pos] + else: + # конец цепочки + break + + np_verts = np.array(verts_in) + verts_out = np_verts[chain].tolist() + return verts_out + pass + +def separate_loose_mesh(verts_in, poly_edge_in): + ''' separate a mesh by loose parts. + input: + 1. list of verts + 2. list of edges/polygons + output: list of + 1. separated list of verts + 2. separated list of edges/polygons with new indices of separated elements + 3. separated list of edges/polygons (like 2) with old indices + ''' + verts_out = [] + poly_edge_out = [] + poly_edge_old_indexes_out = [] # faces with old indices + + # build links + node_links = {} + for edge_face in poly_edge_in: + for i in edge_face: + if i not in node_links: + node_links[i] = set() + node_links[i].update(edge_face) + + nodes = set(node_links.keys()) + n = nodes.pop() + node_set_list = [set([n])] + node_stack = collections.deque() + node_stack_append = node_stack.append + node_stack_pop = node_stack.pop + node_set = node_set_list[-1] + # find separate sets + while nodes: + for node in node_links[n]: + if node not in node_set: + node_stack_append(node) + if not node_stack: # new mesh part + n = nodes.pop() + node_set_list.append(set([n])) + node_set = node_set_list[-1] + else: + while node_stack and n in node_set: + n = node_stack_pop() + nodes.discard(n) + node_set.add(n) + # create new meshes from sets, new_pe is the slow line. + if len(node_set_list) >= 1: + for node_set in node_set_list: + mesh_index = sorted(node_set) + vert_dict = {j: i for i, j in enumerate(mesh_index)} + new_vert = [verts_in[i] for i in mesh_index] + new_pe = [[vert_dict[n] for n in fe] + for fe in poly_edge_in + if fe[0] in node_set] + old_pe = [fe for fe in poly_edge_in + if fe[0] in node_set] + verts_out.append(new_vert) + poly_edge_out.append(new_pe) + poly_edge_old_indexes_out.append(old_pe) + elif node_set_list: # no reprocessing needed + verts_out.append(verts_in) + poly_edge_out.append(poly_edge_in) + poly_edge_old_indexes_out.append(poly_edge_in) + + return verts_out, poly_edge_out, poly_edge_old_indexes_out + +def ShowMessageBox(message = "", title = "Message Box", icon = 'INFO'): + def draw(self, context): + self.layout.label(text=message) + bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) + + +class SvSaveCGALDatFile(bpy.types.Operator, SvGenericNodeLocator): + ''' Save coords and angles to the file .dat for CGAL ''' + bl_idname = "node.sverchok_save_cgal_dat_file" + bl_label = "Save coords and angles to the file .dat for CGAL" + + def sv_execute(self, context, node): + if hasattr(node, 'saveCGALDatFile')==True: + node.saveCGALDatFile() + #text = node.dataAsString() + #context.window_manager.clipboard = text + ShowMessageBox("File saved") + pass + + +class SvStraightSkeleton2D(ModifierLiteNode, SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Merge two 2d meshes + + Each mesh can have disjoint parts + Only X and Y coordinate takes in account + """ + bl_idname = 'SvStraightSkeleton2D' + bl_label = 'Straight Skeleton 2D' + bl_icon = 'MOD_OUTLINE' + + def wrapper_tracked_ui_draw_op(self, layout_element, operator_idname, **keywords): + """ + this wrapper allows you to track the origin of a clicked operator, by automatically passing + the node_name and tree_name to the operator. + + example usage: + + row.separator() + self.wrapper_tracked_ui_draw_op(row, "node.view3d_align_from", icon='CURSOR', text='') + + """ + op = layout_element.operator(operator_idname, **keywords) + op.node_name = self.name + op.tree_name = self.id_data.name + return op + + ss_angle: FloatProperty( + default=0.785398, name="a float", update=updateNode, + description = "Angle of cliff", + subtype='ANGLE', + ) # type: ignore + + ss_height: FloatProperty( + default=1.0, name="a float", update=updateNode, + description = "Max height", + #subtype='ANGLE', + ) # type: ignore + + merge_meshes: BoolProperty( + name='Merge', + description='Apply modifier geometry to import (original untouched)', + default=True, update=updateNode) # type: ignore + + verbose_messages_while_process: BoolProperty( + name='Verbose', + description='Show additional debug info in console', + default=True, update=updateNode) # type: ignore + + + def draw_failed_contours_vertices_out_socket(self, socket, context, layout): + #layout.prop(self, 'enable_verts_attribute_sockets', icon='STICKY_UVS_DISABLE', text='', toggle=True) + if socket.objects_number>0: + layout.label(text=f'', icon='ERROR') + layout.label(text=f'{socket.label} ') + if socket.is_linked: # linked INPUT or OUTPUT + layout.label(text=f". {socket.objects_number or ''}") + elif socket.is_output: # unlinked OUTPUT + layout.separator() + + + def draw_buttons(self, context, layout): + col = layout.column() + col.prop(self, 'ss_angle') + #col.prop(self, 'ss_height') + col.prop(self, 'merge_meshes') + col.prop(self, 'verbose_messages_while_process') + ui_file_save_dat = col.row() + self.wrapper_tracked_ui_draw_op(ui_file_save_dat, SvSaveCGALDatFile.bl_idname, text='', icon='DISK_DRIVE') + + + pass + + def draw_buttons_ext(self, context, layout): + col = layout.column(align=True) + pass + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', 'vertices') + self.inputs.new('SvStringsSocket' , 'edges') + self.inputs.new('SvStringsSocket' , 'polygons') + self.inputs.new('SvStringsSocket' , 'ssangles').prop_name = 'ss_angle' + self.inputs.new('SvStringsSocket' , 'ssheight').prop_name = 'ss_height' + self.inputs.new('SvTextSocket' , 'file_name') + #self.inputs.new('SvMatrixSocket' , "matrixes") + + self.inputs['vertices'].label = 'Vertices' + self.inputs['edges'].label = 'Edges' + self.inputs['polygons'].label = 'Polygons' + self.inputs['ssangles'].label = 'Angles' + self.inputs['ssheight'].label = 'Height' + self.inputs['file_name'].label = 'File Name' + #self.inputs['matrixes'].label = 'Matrixes' + + self.outputs.new('SvVerticesSocket', 'vertices') + self.outputs.new('SvStringsSocket' , 'edges') + self.outputs.new('SvStringsSocket' , 'polygons') + self.outputs.new('SvVerticesSocket', 'boundaries') + self.outputs.new('SvVerticesSocket', 'objects_boundaries_vertices') + self.outputs.new('SvVerticesSocket', 'failed_contours_vertices') + + self.outputs['vertices'].label = 'Vertices' + self.outputs['edges'].label = 'Edges' + self.outputs['polygons'].label = 'Polygons' + self.outputs['boundaries'].label = 'Boundaries' + self.outputs['objects_boundaries_vertices'].label = 'Object Boud.Verts.' + self.outputs['failed_contours_vertices'].label = 'Wrong contours verts' + self.outputs['failed_contours_vertices'].custom_draw = 'draw_failed_contours_vertices_out_socket' + + def process(self): + if not all([sock.is_linked for sock in self.inputs if sock.name in ['vertices', 'edges', 'polygons'] ]): + return + if not any([sock.is_linked for sock in self.outputs]): + return + + #print(f"== stright skeleton node start ===============================") + inputs = self.inputs + _Vertices = inputs['vertices'].sv_get(default=[[]], deepcopy=False) + Vertices3 = ensure_nesting_level(_Vertices, 3) + _Edges = inputs['edges'].sv_get(default=[[]], deepcopy=False) + Edges3 = ensure_nesting_level(_Edges, 3) + _Faces = inputs['polygons'].sv_get(default=[[]], deepcopy=False) + Faces3 = ensure_nesting_level(_Faces, 3) + _ssangles = inputs['ssangles'].sv_get(default=[[self.ss_angle]], deepcopy=False) + ssangles3 = ensure_nesting_level(_ssangles, 3) + _ssheights = inputs['ssheight'].sv_get(default=[[self.ss_height]], deepcopy=False) + ssheights3 = ensure_nesting_level(_ssheights, 3) + + _file_names = inputs['file_name'].sv_get(default=[[]], deepcopy=False) + file_names3 = ensure_nesting_level(_file_names, 3) + file_name_dat = None + if len(file_names3)>0 and len(file_names3[0])>0 and len(file_names3[0][0])>0: + file_name_dat = file_names3[0][0][0] + + res_verts = [] + res_boundaries_verts = [] + res_edges = [] + res_faces = [] + + objects_boundaries = [] + objects_heights = [] + objects_angles_of_boundaries = [] + objects_area_boundaries = [] + + contours_failed_at_all = [] + + for verts_i, edges_i, faces_i, ssangle3, ssheight3 in zip_long_repeat(Vertices3, Edges3, Faces3, ssangles3, ssheights3): + ssheight = ssheight3[0][0] + # separate objects of loose parts (objects can has islands. Every island have to be separated) + verts_i_separated, faces_i_separated, _ = separate_loose_mesh(verts_i, faces_i) + + for I in range(len(verts_i_separated)): + verts_i_separated_I, faces_i_separated_I = verts_i_separated[I], faces_i_separated[I] + + try: + bm = bmesh_from_pydata(verts_i_separated_I, None, faces_i_separated_I, normal_update=True) + bm.edges.ensure_lookup_table() + edges = [[e.verts[0].index, e.verts[1].index] for e in bm.edges] + BoundaryEdges, NonBoundaryEdges, MaskBoundaryEdges = Edges.process(bm, "Boundary", edges) + # BoundaryVerticesIndexes = [] + # for e in BoundaryEdges: + # BoundaryVerticesIndexes.extend(e) + # BoundaryVerticesIndexes = set(BoundaryVerticesIndexes) + # verts_i_separated_I = [verts_i_separated_I[I] for I in BoundaryVerticesIndexes] + bm.free() + except Exception as ex: + # Keep shape to show as errors in the future + contours_failed_at_all.append(verts_i_separated_I) + continue + + # separate contours of every island + verts_boundaries, edges_boundaries, _ = separate_loose_mesh(verts_i_separated_I, BoundaryEdges) + + object_boundaries = [] + object_area_boundaries = [] + objects_angles_of_boundary = [] + failed_contours_vertices = [] + areas = [] + for I in range(len(verts_boundaries)): + verts_boundaries_I, edges_boundaries_I = verts_boundaries[len(verts_boundaries)-1-I], edges_boundaries[len(verts_boundaries)-1-I] + #vect_boundaries_I_sorted, pol_edge_new, index_new = sort_vertices_by_connexions(verts_boundaries_I, edges_boundaries_I, True) + vect_boundaries_I_sorted = vertices_sort_by_edges(verts_boundaries_I, edges_boundaries_I) + res_boundaries_verts.append(vect_boundaries_I_sorted) + area = areas_from_polygons(vect_boundaries_I_sorted, [list(range(len(vect_boundaries_I_sorted)))], ) + areas.append(area[0]) + object_boundaries.append(vect_boundaries_I_sorted) + object_area_boundaries.append({"area":area, "object_boundaries":vect_boundaries_I_sorted}) + objects_angles_of_boundary.append( [self.ss_angle*180/math.pi,]*len(verts_boundaries_I) ) + pass + srt = sort_together([areas, object_boundaries, objects_angles_of_boundary]) + object_boundaries_sorted, objects_angles_of_boundary_sorted = list(reversed(srt[1])), list(reversed(srt[2])) + objects_boundaries.append(object_boundaries_sorted) + objects_heights.append(ssheight) + objects_angles_of_boundaries.append( objects_angles_of_boundary_sorted ) + objects_area_boundaries.append(object_area_boundaries) + pass + pass + + + if not file_name_dat: + lst_errors = [] + def parallel_extrude_skeleton(data1): + #new_mesh = pySVCGAL_extrude_skeleton(self.ss_height/1.0, objects_boundaries_I, objects_angles_of_boundaries_I, True) + new_mesh = pySVCGAL_extrude_skeleton( *data1 ) + return new_mesh + with ThreadPoolExecutor() as executor: + data = [] + data_copy = [] + for I in range(len(objects_boundaries)): + objects_boundaries_I = objects_boundaries[I] + objects_heights_I = objects_heights[I] + objects_angles_of_boundaries_I = objects_angles_of_boundaries[I] + data .append( [I, objects_heights_I/1.0, objects_boundaries_I, objects_angles_of_boundaries_I, self.verbose_messages_while_process] ) + data_copy.append( {'polygon_id':I, 'height':objects_heights_I/1.0, 'boundaries' : objects_boundaries_I, 'angles' : objects_angles_of_boundaries_I, 'verbose' : self.verbose_messages_while_process} ) + + # run all skeletons + data_processed = list( executor.map(parallel_extrude_skeleton, data)) + faces_delta = 0 + object_verts = [] + object_faces = [] + lst_errors1 = [] + for data1 in data_processed: + polygon_id = data1['polygon_id'] + if data1['has_error']==True: + if 'ftcs_count' in data1 and data1['ftcs_count']>0: + lst_errors1.append(f"Failed polygon_id: {polygon_id}. ") + if data1['str_error']: + lst_errors1[-1] = lst_errors1[-1] + (data1['str_error']) + for s in data1['ftcs_vertices_description']: + if s: + lst_errors1.append(f'{s}') + failed_contours_vertices.extend(data1['ftcs_vertices_list']) + pass + else: + #print(f"\nPolygon_id: {polygon_id} is good. It has no errors") + if self.merge_meshes==True: + object_verts.extend(data1['vertices']) + object_faces.extend( [ list(map(lambda n: n+faces_delta, face)) for face in data1['faces'] ] ) + faces_delta+=len(data1['vertices']) + else: + res_verts.append( data1['vertices'] ) + res_faces.append( data1['faces'] ) + pass + lst_errors.extend(lst_errors1) + + if self.merge_meshes==True: + res_verts.append(object_verts) + res_faces.append(object_faces) + # else: + # res_verts.extend(object_verts) + # res_faces.extend(object_faces) + if len(contours_failed_at_all)>0: + failed_contours_vertices.extend(contours_failed_at_all) + pass + + print("") + print("Node Skeleton Finished.") + if lst_errors: + for s in lst_errors: + print(s) + if self.verbose_messages_while_process==False: + print("") + print("for more info turn on verbose mode in node") + + + else: # file_name_dat: + # faces_delta = 0 + # object_verts = [] + # object_faces = [] + # for I in range(len(objects_boundaries)): + # #print(f"== stright skeleton {I} ===============================") + # objects_boundaries_I = objects_boundaries[I] + # objects_angles_of_boundaries_I = objects_angles_of_boundaries[I] + # new_mesh = pySVCGAL_extrude_skeleton(self.ss_height/1.0, objects_boundaries_I, objects_angles_of_boundaries_I, True) + # object_verts.extend(new_mesh['vertices']) + # object_faces.extend( [ list(map(lambda n: n+faces_delta, face)) for face in new_mesh['faces'] ] ) + # faces_delta+=len(new_mesh['vertices']) + # # res_verts.append(new_mesh['vertices']) + # # res_faces.append(new_mesh['faces']) + # pass + # res_verts.append(object_verts) + # res_faces.append(object_faces) + + lines_verts = [] + lines_angles = [] + # Записывать вершины только первого объекта, т.к. только один объект и может быть рассчитал в CGAL + # Когда сделаю компонент, то тогда передам все объекты по очереди. + objects_boundaries_0 = objects_boundaries[0] + for I in range(len(objects_boundaries_0)): + objects_boundaries_0_I = objects_boundaries_0[I] + lines_verts .append(str(len(objects_boundaries_0_I)),) + if len(objects_boundaries_0_I)>0: + # Если контур только один, внешний, то добавление количества углов приводит к сбою. + # При обном контуре не добавлять количество углов в первую строку + lines_angles.append(str(len(objects_boundaries_0_I)),) + + for J, vert in enumerate(objects_boundaries_0_I): + v_str = [str(v) for v in vert[:2] ] + v_line = " ".join(v_str) + lines_verts.append(v_line) + lines_angles.append( str(self.ss_angle*180/math.pi) ) + txt_verts = "\n".join(lines_verts) + txt_angles = "\n".join(lines_angles) + + print(f"stright skeleton node write to file") + with open(file_name_dat, "w") as file: + file.write(txt_verts) + print(f'Записаны вершины {len(lines_verts)-1}: {file_name_dat}') + with open(file_name_dat+'.angles', "w") as file: + file.write(txt_angles) + print(f'Записаны углы: {len(lines_angles)-1}: {file_name_dat}.angles') + + self.outputs['vertices'].sv_set(res_verts) + self.outputs['edges'].sv_set(res_edges) + self.outputs['polygons'].sv_set(res_faces) + self.outputs['boundaries'].sv_set(res_boundaries_verts) + self.outputs['objects_boundaries_vertices'].sv_set(objects_boundaries) + self.outputs['failed_contours_vertices'].sv_set(failed_contours_vertices) + + pass + + def saveCGALDatFile(self): + if not all([sock.is_linked for sock in self.inputs if sock.name not in ['ssangles', 'file_name'] ]): + return 'Не подключены сокеты Vertices и Faces. Файлы не записаны.' + + print("file .dat saved") + pass + +classes = [SvSaveCGALDatFile, SvStraightSkeleton2D,] +register, unregister = bpy.utils.register_classes_factory(classes) \ No newline at end of file From 72fdb28a94a1e9e8c7419eb6743a8bfce707bd44 Mon Sep 17 00:00:00 2001 From: satabol Date: Fri, 11 Oct 2024 20:09:44 +0300 Subject: [PATCH 2/7] - make documentation for not SvStraightSkeleton2D --- dependencies.py | 8 + docs/nodes/CAD/stright_skeleton_2d.rst | 178 ++++++ index.yaml | 1 - menus/full_by_data_type.yaml | 2 + menus/full_nortikin.yaml | 3 + nodes/CAD/straight_skeleton_2d_test001.py | 655 ---------------------- nodes/CAD/stright_skeleton_2d.py | 400 +++++++++---- settings.py | 1 + 8 files changed, 478 insertions(+), 770 deletions(-) create mode 100644 docs/nodes/CAD/stright_skeleton_2d.rst delete mode 100644 nodes/CAD/straight_skeleton_2d_test001.py diff --git a/dependencies.py b/dependencies.py index 31cdfe1d8c..b22581136b 100644 --- a/dependencies.py +++ b/dependencies.py @@ -209,6 +209,14 @@ def draw_message(box, package, dependencies=None): except ImportError: pyQuadriFlow = None +pySVCGAL_d = sv_dependencies["pySVCGAL"] = SvDependency("pySVCGAL","https://github.com/satabol/pySVCGAL") +pySVCGAL_d.pip_installable = True +try: + import pySVCGAL + pySVCGAL_d.module = pySVCGAL +except ImportError: + pySVCGAL = None + settings.pip = pip settings.sv_dependencies = sv_dependencies settings.ensurepip = ensurepip diff --git a/docs/nodes/CAD/stright_skeleton_2d.rst b/docs/nodes/CAD/stright_skeleton_2d.rst new file mode 100644 index 0000000000..48a6e0fc8b --- /dev/null +++ b/docs/nodes/CAD/stright_skeleton_2d.rst @@ -0,0 +1,178 @@ +Stright Skeleton 2d +=================== + +.. image:: https://github.com/user-attachments/assets/2a141705-62e6-489b-a4be-9333df8c7951 + :target: https://github.com/user-attachments/assets/2a141705-62e6-489b-a4be-9333df8c7951 + +Functionality +------------- + +This node is a python wrapper of function "Skeleton Extrude 2d" of CGAL https://doc.cgal.org/latest/Straight_skeleton_2/index.html. + +This package implements weighted straight skeletons for two-dimensional polygons with holes. +An intuitive way to think of the construction of straight skeletons is to imagine that wavefronts +(or grassfires) are spawned at each edge of the polygon, and are moving inward. As the fronts progress, +they either contract or expand depending on the angles formed between polygon edges, and sometimes +disappear. Under this transformation, polygon vertices move along the angular bisector of the lines +subtending the edges, tracing a tree-like structure, the straight skeleton. + +.. image:: https://github.com/user-attachments/assets/774128fa-5a89-4d54-ba78-89e7ac10f274 + :target: https://github.com/user-attachments/assets/774128fa-5a89-4d54-ba78-89e7ac10f274 + +.. image:: https://github.com/user-attachments/assets/8baaadf0-1e09-454a-af6a-85517db3bdb4 + :target: https://github.com/user-attachments/assets/50fd85bb-db65-41d3-a536-142c2cefffac + +Inputs +------ + +- Vertices, Edges, Faces - Input Mesh (2D only) + .. image:: https://github.com/user-attachments/assets/3ce5a747-9b8d-4aef-94fc-9e268425e6a6 + :target: https://github.com/user-attachments/assets/3ce5a747-9b8d-4aef-94fc-9e268425e6a6 + +- Taper Angle - Angle between plane and Face that this algorithm will build. + + .. image:: https://github.com/user-attachments/assets/666c3a16-f124-4230-906b-8b4ea2cd699c + :target: https://github.com/user-attachments/assets/666c3a16-f124-4230-906b-8b4ea2cd699c + + This parameter has only radians value but you can change its view in Blender Settings: + + .. image:: https://github.com/user-attachments/assets/7828d4c0-eac3-4c7d-992e-c527cdcbfbe0 + :target: https://github.com/user-attachments/assets/7828d4c0-eac3-4c7d-992e-c527cdcbfbe0 + +| + +If you do not connect any lists of floats values then this value will be used for every objects +connected into this node: + + .. image:: https://github.com/user-attachments/assets/51ccfb1a-30c5-43ed-9b9f-b2e1c6402cb8 + :target: https://github.com/user-attachments/assets/51ccfb1a-30c5-43ed-9b9f-b2e1c6402cb8 + +| + +If you connect list of floats then it will be used per objects: + + .. image:: https://github.com/user-attachments/assets/f40f4f4f-92dd-4d41-9eae-ddb890214fb6 + :target: https://github.com/user-attachments/assets/f40f4f4f-92dd-4d41-9eae-ddb890214fb6 + +- Height - Height of object or objects. If used single value then this value vill be used for every objects. If socket is connected with float values then values will be used per objects: + +.. raw:: html + + + +| + +- Mask of objects - Mask hide objects. If element of boolean mask is True then object are hidden. If length of mask is more than length of objects then exceeded values will be omitted. + +.. raw:: html + + + +| + + You can use index mask of integer values. If index is out of count of objects then it will be omitted. Equals values are merged. + +.. raw:: html + + + +| +| + +Parameters +---------- + +.. image:: https://github.com/user-attachments/assets/0119d5b9-09d2-49a4-b4fd-91e0afdcf76c + :target: https://github.com/user-attachments/assets/0119d5b9-09d2-49a4-b4fd-91e0afdcf76c + +- Join mode. Split, Keep, Merge. + - **Split** - If some of objects has several independent meshes then they will be splitten individually and you can get more object on output than on input. (Mask will hide all meshes in multimesh objects) + + .. image:: https://github.com/user-attachments/assets/5d76cd4f-bb2a-4a05-b218-85ae0d96adee + :target: https://github.com/user-attachments/assets/5d76cd4f-bb2a-4a05-b218-85ae0d96adee + + - **Keep** - If some of objects has several independent meshes then they will be as one object on output. + + .. image:: https://github.com/user-attachments/assets/41364d77-ae72-46f6-b4b7-eceaf6bda435 + :target: https://github.com/user-attachments/assets/41364d77-ae72-46f6-b4b7-eceaf6bda435 + + - **Merge** - This node will merge all vertices, edjes, and faces into a single object. + + .. image:: https://github.com/user-attachments/assets/bd119bb8-ad08-4983-be67-d97c20ad8bb3 + :target: https://github.com/user-attachments/assets/bd119bb8-ad08-4983-be67-d97c20ad8bb3 + + - **Exclude Height** - If you want to see objects without height limits just turn it on. All objects will be recalulated without heights limits. + + .. raw:: html + + + + - **Only Tests** - If you have a hi poly mesh like imported SVG file one can save time and do not Skeletonize all meshes before fix all. You can connect viewer draw into the "Wrong Contours Verts" with red color or any color you prefer for errors to see any wrong contrours. Red dots are wrong contours. + + .. image:: https://github.com/user-attachments/assets/e349df88-3e4b-4096-b2f5-2682b13ed48a + :target: https://github.com/user-attachments/assets/e349df88-3e4b-4096-b2f5-2682b13ed48a + + - **Verbose** - On will show more info in console while Extrude Straight Sceleton. Off will show less info. + + .. image:: https://github.com/user-attachments/assets/f71aba10-3d00-48d0-b352-907f20b45ef8 + :target: https://github.com/user-attachments/assets/f71aba10-3d00-48d0-b352-907f20b45ef8 + + +Performance +----------- + +If you have a low poly model then no problem - you can work with that model in real time: + +.. image:: https://github.com/user-attachments/assets/6bb3f564-5773-4458-be44-8e437c1d33d6 + :target: https://github.com/user-attachments/assets/6bb3f564-5773-4458-be44-8e437c1d33d6 + +.. raw:: html + + + +If you try high poly like Besier 2D with many points and hi resolution (1) then better is to turn off (2) update sverchok nodes while editing objects and run process manually (3): + +.. image:: https://github.com/user-attachments/assets/7103fb0d-3ad2-477a-8364-8997722c261c + :target: https://github.com/user-attachments/assets/7103fb0d-3ad2-477a-8364-8997722c261c + +Examples +======== + +Hexagon with Stright Skeleton +----------------------------- + +.. image:: https://github.com/user-attachments/assets/61342e4d-7a10-4903-90e9-5e654db42dae + :target: https://github.com/user-attachments/assets/61342e4d-7a10-4903-90e9-5e654db42dae + +.. image:: https://github.com/user-attachments/assets/57e801d4-e46f-49e8-9831-728be1628c82 + :target: https://github.com/user-attachments/assets/57e801d4-e46f-49e8-9831-728be1628c82 + + +Palm Tree +--------- + +Src: https://www.143vinyl.com/free-svg-download-palm-trees.html + +.. image:: https://github.com/user-attachments/assets/3911de50-2708-411b-aedf-6427e1a0131b + :target: https://github.com/user-attachments/assets/3911de50-2708-411b-aedf-6427e1a0131b + +Src: https://www.templatesarea.com/celtic-tree-of-life-silhouettes-free-vector-graphics/ + +.. image:: https://github.com/user-attachments/assets/6527588d-a89e-4b04-8965-9450014cc0ba + :target: https://github.com/user-attachments/assets/6527588d-a89e-4b04-8965-9450014cc0ba + diff --git a/index.yaml b/index.yaml index ce05df1f39..97cec560bd 100644 --- a/index.yaml +++ b/index.yaml @@ -496,7 +496,6 @@ - SvWafelNode - --- - SvStraightSkeleton2D - - SvStraightSkeleton2DTest001 - --- diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index 97a51d0b94..c868948cc5 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -178,6 +178,8 @@ - SvCropMesh2D - SvCSGBooleanNodeMK2 - SvWafelNode + - --- + - SvStraightSkeleton2D - Analyze Mesh: - extra_menu: MeshPartialMenu # to make the category available in another menu (1,2,3,4,5) diff --git a/menus/full_nortikin.yaml b/menus/full_nortikin.yaml index 318395f5d5..bd7a83c133 100644 --- a/menus/full_nortikin.yaml +++ b/menus/full_nortikin.yaml @@ -533,6 +533,9 @@ - SvCropMesh2D - SvCSGBooleanNodeMK2 - SvWafelNode + - --- + - SvStraightSkeleton2D + - C CURVES: - icon_name: OUTLINER_OB_CURVE - extra_menu: AdvancedObjectsPartialMenu diff --git a/nodes/CAD/straight_skeleton_2d_test001.py b/nodes/CAD/straight_skeleton_2d_test001.py deleted file mode 100644 index bf644a3ec9..0000000000 --- a/nodes/CAD/straight_skeleton_2d_test001.py +++ /dev/null @@ -1,655 +0,0 @@ -# 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 bpy - -import bpy, math, bmesh -from mathutils import Vector, Matrix -from collections import namedtuple - -from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode -from sverchok.utils.geom_2d.merge_mesh import merge_mesh -from sverchok.utils.nodes_mixins.sockets_config import ModifierLiteNode -from sverchok.data_structure import dataCorrect, updateNode, zip_long_repeat, ensure_nesting_level, flatten_data - -## internal.py ######################################### -Plane = namedtuple('Plane', 'normal distance') - -def normalOfPolygon(vertices): - normal = Vector((0.0, 0.0, 0.0)) - for index, current in enumerate(vertices): - prev = vertices[index-1] - normal += (prev-vertices[0]).cross(current-vertices[0]) - return normal - -def areaOfPolygon(vertices): - return normalOfPolygon(vertices).length*0.5 - -def linePlaneIntersection(origin, dir, plane): - # return mathutils.geometry.intersect_line_plane(origin, origin+dir, plane.normal*plane.distance, plane.normal) - det = dir@plane.normal - return float('nan') if det == 0 else (plane.distance-origin@plane.normal)/det - -def planePlaneIntersection(planeA, planeB, tollerance=0.0001): - # return mathutils.geometry.intersect_plane_plane(planeA.normal*planeA.distance, planeA.normal, planeB.normal*planeB.distance, planeB.normal) - if 1.0-abs(planeA.normal@planeB.normal) < tollerance: - return ('Parallel' if abs(planeA.distance-planeB.distance) > tollerance else 'Coplanar', None, None) - dir = planeA.normal.cross(planeB.normal).normalized() - ray_origin = planeA.normal*planeA.distance - ray_dir = planeA.normal.cross(dir) - origin = ray_origin+ray_dir*linePlaneIntersection(ray_origin, ray_dir, planeB) - return ('Intersecting', origin, dir) - -def linePointDistance(begin, dir, point): - return (point-begin).cross(dir.normalized()).length - -def nearestPointOfLines(originA, dirA, originB, dirB, param_tollerance=0.0, dist_tollerance=0.001): - # https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points - normal = dirA.cross(dirB) - normalA = dirA.cross(normal) - normalB = dirB.cross(normal) - divisorA = dirA@normalB - divisorB = dirB@normalA - originAB = originB-originA - if abs(divisorA) <= param_tollerance or abs(divisorB) <= param_tollerance: - if dirA@dirA == 0.0 or dirB@dirB == 0.0 or linePointDistance(originA, dirA, originB) >= dist_tollerance: - return ('Parallel', float('nan'), float('nan')) - paramA = originAB@dirA/(dirA@dirA) - paramB = -originAB@dirB/(dirB@dirB) - return ('Coaxial', paramA, paramB) - else: - paramA = originAB@normalB/divisorA - paramB = -originAB@normalA/divisorB - nearestPointA = originA+dirA*paramA - nearestPointB = originB+dirB*paramB - return ('Crossing' if (nearestPointA-nearestPointB).length <= dist_tollerance else 'Skew', paramA, paramB) - -def lineSegmentLineSegmentIntersection(lineAVertexA, lineAVertexB, lineBVertexA, lineBVertexB): - dirA = lineAVertexB-lineAVertexA - dirB = lineBVertexB-lineBVertexA - type, paramA, paramB = nearestPointOfLines(lineAVertexA, dirA, lineBVertexA, dirB) - if type == 'Parallel' or type == 'Skew': - return (float('nan'), float('nan')) - if type == 'Coaxial': - if paramA < 0.0 and paramB < 0.0: # Facing away from one another - return (float('nan'), float('nan')) - if paramA > 0.0 and paramB > 0.0: # Facing towards each other - if paramA > 1.0 and (lineBVertexB-lineAVertexA)@dirA > 1.0: # End of B is not in A - return (float('nan'), float('nan')) - elif paramA > 1.0 or paramB > 1.0: # One is chasing the other but out of reach - return (float('nan'), float('nan')) - paramA = max(0.0, (lineBVertexB-lineAVertexA)@dirA/(dirA@dirA)) - paramB = max(0.0, (lineAVertexB-lineBVertexA)@dirB/(dirB@dirB)) - return (paramA, paramB) - if paramA < 0.0 or paramA > 1.0 or paramB < 0.0 or paramB > 1.0: # Intersection is outside the line segments - return (float('nan'), float('nan')) - return (paramA, paramB) - -def rayLineSegmentIntersection(originA, dirA, lineVertexA, lineVertexB): - dirB = lineVertexB-lineVertexA - type, paramA, paramB = nearestPointOfLines(originA, dirA, lineVertexA, dirB) - if type == 'Parallel' or type == 'Skew': - return float('nan') - if type == 'Coaxial': - if paramA > 0.0: - return paramA if (paramB < 0.0) else max(0.0, (lineVertexB-originA)@dirA/(dirA@dirA)) - else: - return float('nan') if (paramB < 0.0 or paramB > 1.0) else 0.0 - if paramA < 0.0 or paramB < 0.0 or paramB > 1.0: # Intersection is behind the rays origin or outside of the line segment - return float('nan') - return paramA - -def rayRayIntersection(originA, dirA, originB, dirB): - type, paramA, paramB = nearestPointOfLines(originA, dirA, originB, dirB) - if type == 'Parallel' or type == 'Skew': - return (float('nan'), float('nan')) - if type == 'Coaxial': - if paramA < 0.0 and paramB < 0.0: # Facing away from one another - return (float('nan'), float('nan')) - if paramA > 0.0 and paramB > 0.0: # Facing towards each other - paramSum = paramA+paramB - paramA = paramA*paramA/paramSum - paramB = paramB*paramB/paramSum - return (paramA, paramB) - return (paramA, 0.0) if paramA > 0.0 else (0.0, paramB) # One is chasing the other - if paramA < 0.0 or paramB < 0.0: # Intersection is behind the rays origins - return (float('nan'), float('nan')) - return (paramA, paramB) - -def insort_right(sorted_list, keyfunc, entry, lo=0, hi=None): - if hi == None: - hi = len(sorted_list) - while lo < hi: - mid = (lo+hi)//2 - if keyfunc(entry) < keyfunc(sorted_list[mid]): - hi = mid - else: - lo = mid+1 - sorted_list.insert(lo, entry) - - - -def selectedPolygons(src_obj): - polygons = [] - in_edit_mode = (src_obj.mode == 'EDIT') - bpy.ops.object.mode_set(mode='OBJECT') - if src_obj.type == 'CURVE': - if in_edit_mode: - splines = [] - for spline in bpy.context.object.data.splines: - selected = True - if spline.type == 'POLY': - for index, point in enumerate(spline.points): - if point.select == False: - selected = False - break - if selected: - splines.append(spline) - else: - splines = src_obj.data.splines - for spline in splines: - polygons.append(list(point.co.xyz for point in spline.points)) - else: - loops = [] - for face in src_obj.data.polygons: - if in_edit_mode and not face.select: - continue - polygons.append(list(src_obj.data.vertices[vertex_index].co for vertex_index in face.vertices)) - return polygons - -def addObject(type, name): - if type == 'CURVE': - data = bpy.data.curves.new(name=name, type='CURVE') - data.dimensions = '3D' - elif type == 'MESH': - data = bpy.data.meshes.new(name=name) - obj = bpy.data.objects.new(name, data) - obj.location = bpy.context.scene.cursor.location - bpy.context.scene.collection.objects.link(obj) - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - return obj - -def addPolygonSpline(obj, cyclic, vertices, weights=None, select=False): - spline = obj.data.splines.new(type='POLY') - spline.use_cyclic_u = cyclic - spline.points.add(len(vertices)-1) - for index, point in enumerate(spline.points): - point.co.xyz = vertices[index] - point.select = select - if weights: - point.weight_softbody = weights[index] - return spline - - - -class SlabIntersection: - __slots__ = ['prev_slab', 'next_slab', 'origin', 'dir', 'begin_param', 'end_param'] - def __init__(self, prev_slab, next_slab, origin, dir, begin_param, end_param): - self.prev_slab = prev_slab - self.next_slab = next_slab - self.origin = origin - self.dir = dir - self.begin_param = begin_param - self.end_param = end_param - - def reverse(self): - self.dir *= -1.0 - [self.begin_param, self.end_param] = [-self.end_param, -self.begin_param] - - def otherSlab(self, slab): - return self.prev_slab if slab == self.next_slab else self.next_slab - -class Slab: - __slots__ = ['edge', 'slope', 'plane', 'prev_slab', 'next_slab', 'prev_polygon_vertex', 'next_polygon_vertex', 'prev_lightcycles', 'next_lightcycles', 'vertices', 'slab_intersections'] - def __init__(self, polygon_normal, prev_polygon_vertex, next_polygon_vertex): - self.edge = (next_polygon_vertex-prev_polygon_vertex).normalized() - edge_orthogonal = self.edge.cross(polygon_normal).normalized() - normal = (polygon_normal+edge_orthogonal).normalized() - self.slope = (polygon_normal-edge_orthogonal).normalized() - self.plane = Plane(normal=normal, distance=next_polygon_vertex@normal) - self.prev_lightcycles = [] - self.next_lightcycles = [] - self.prev_polygon_vertex = prev_polygon_vertex - self.next_polygon_vertex = next_polygon_vertex - self.vertices = [self.prev_polygon_vertex, self.next_polygon_vertex] - self.slab_intersections = [] - - def isOuterOfCollision(self, in_dir, out_dir, polygon_normal): - normal = in_dir.cross(polygon_normal) - return (normal@self.plane.normal > 0.0) == (normal@out_dir > 0.0) - - def calculateVerticesFromLightcycles(self): - def handleSide(lightcycles, prepend): - for lightcycle in lightcycles: - if lightcycle.slab_intersection.origin is None or lightcycle.slab_intersection.dir is None or lightcycle.slab_intersection.end_param is None: - pass - else: - vertex = lightcycle.slab_intersection.origin+lightcycle.slab_intersection.dir*lightcycle.slab_intersection.end_param - if prepend: - self.vertices.insert(0, vertex) - else: - self.vertices.append(vertex) - handleSide(self.prev_lightcycles, True) - handleSide(self.next_lightcycles, False) - - def rayBoundaryIntersection(self, origin, dir, tollerance=0.0001): - intersections = [] - for i in range(0, len(self.vertices)+1): - is_last = (i == 0 or i == len(self.vertices)) - type, paramA, paramB = nearestPointOfLines(origin, dir, self.vertices[0 if i == 0 else i-1], self.slope if is_last else self.vertices[i]-self.vertices[i-1]) - if type == 'Crossing': - if paramB > -tollerance and (is_last or paramB < 1.0+tollerance): - intersections.append((i, paramA)) - elif type == 'Coaxial': - assert(not is_last) - intersections.append((i-1, paramA)) - intersections.append((i, (self.vertices[i]-origin)@dir/(dir@dir))) - intersections.sort(key=lambda entry: entry[1]) - i = 1 - while i < len(intersections): - if intersections[i][1]-intersections[i-1][1] < tollerance: - del intersections[i] - else: - i += 1 - return intersections - - def calculateSlabIntersection(self, other_slab, is_first, tollerance=0.0001): - lightcycles = self.next_lightcycles if is_first else self.prev_lightcycles - if len(lightcycles) > 0 and (lightcycles[0].slab_intersection.prev_slab == other_slab or lightcycles[0].slab_intersection.next_slab == other_slab): - return - type, origin, dir = planePlaneIntersection(self.plane, other_slab.plane) - if type != 'Intersecting': - if self.prev_slab == other_slab or self.next_slab == other_slab: - slab_intersection = SlabIntersection(self, other_slab, self.prev_polygon_vertex if self.prev_slab == other_slab else self.next_polygon_vertex, self.slope, 0.0, float('inf')) - self.slab_intersections.append(slab_intersection) - other_slab.slab_intersections.append(slab_intersection) - return - intersectionsA = self.rayBoundaryIntersection(origin, dir) - intersectionsB = other_slab.rayBoundaryIntersection(origin, dir) - if len(intersectionsA) == 2 and len(intersectionsB) == 2: - begin_param = max(intersectionsA[0][1], intersectionsB[0][1]) - end_param = min(intersectionsA[1][1], intersectionsB[1][1]) - if begin_param < end_param and end_param-begin_param >= tollerance: - slab_intersection = SlabIntersection(self, other_slab, origin+begin_param*dir, dir, 0.0, end_param-begin_param) - self.slab_intersections.append(slab_intersection) - other_slab.slab_intersections.append(slab_intersection) - - def calculateVerticesFromIntersections(self, tollerance=0.001): - pivot = self.prev_polygon_vertex - current_line = None - for candidate in self.slab_intersections: - if candidate.prev_slab == self.prev_slab or candidate.next_slab == self.prev_slab: - current_line = candidate - break - if current_line == None: - print('ERROR: calculateVerticesFromIntersections() could not find the first current_line') - return - if abs((current_line.origin+current_line.dir*current_line.begin_param-pivot)@current_line.dir) > abs((current_line.origin+current_line.dir*current_line.end_param-pivot)@current_line.dir): - current_line.reverse() - self.vertices = [self.prev_polygon_vertex, self.next_polygon_vertex] - while current_line.prev_slab != self.next_slab and current_line.next_slab != self.next_slab: - self.slab_intersections.remove(current_line) - pivot_param = (pivot-current_line.origin)@current_line.dir - best_candidate = None - best_param = float('nan') - current_other_slab = current_line.otherSlab(self) - lightcycles = [] - if len(current_other_slab.prev_lightcycles) > 0: - lightcycles.append(current_other_slab.prev_lightcycles[-1]) - if len(current_other_slab.next_lightcycles) > 0: - lightcycles.append(current_other_slab.next_lightcycles[-1]) - for lightcycle in lightcycles: - if lightcycle.slab_intersection.origin is None or lightcycle.slab_intersection.dir is None: - pass - else: - param = linePlaneIntersection(lightcycle.slab_intersection.origin, lightcycle.slab_intersection.dir, self.plane) - if lightcycle.slab_intersection.begin_param-tollerance <= param and param <= lightcycle.slab_intersection.end_param+tollerance: - candidate_other_slab = lightcycle.slab_intersection.otherSlab(current_other_slab) - position = lightcycle.slab_intersection.origin+lightcycle.slab_intersection.dir*param - param = (position-pivot)@current_line.dir - if candidate_other_slab != self and param > 0.0: - for candidate in self.slab_intersections: - if candidate.otherSlab(self) == candidate_other_slab: - best_candidate = candidate - best_param = current_line.end_param - if abs((best_candidate.origin+best_candidate.dir*best_candidate.begin_param-pivot)@best_candidate.dir) > abs((best_candidate.origin+best_candidate.dir*best_candidate.end_param-pivot)@best_candidate.dir): - best_candidate.reverse() - break - for candidate in self.slab_intersections: - if candidate == best_candidate: - continue - type, paramA, paramB = nearestPointOfLines(current_line.origin, current_line.dir, candidate.origin, candidate.dir) - if (type == 'Crossing' or type == 'Coaxial') and pivot_param-tollerance <= paramA and \ - current_line.begin_param-tollerance <= paramA and paramA <= current_line.end_param+tollerance and \ - candidate.begin_param-tollerance <= paramB and paramB <= candidate.end_param+tollerance and \ - (best_candidate == None or best_param > paramA): - best_candidate = candidate - best_param = paramA - normal = self.plane.normal.cross(current_line.dir) - if (best_candidate.origin+best_candidate.dir*best_candidate.begin_param-pivot)@normal < (best_candidate.origin+best_candidate.dir*best_candidate.end_param-pivot)@normal: - best_candidate.reverse() - if best_candidate == None: - print('ERROR: calculateVerticesFromIntersections() could not find the next current_line') - return - pivot = current_line.origin+current_line.dir*best_param - current_line = best_candidate - self.vertices.insert(0, pivot) - self.slab_intersections = None - -class Collision: - __slots__ = ['winner_time', 'looser_time', 'winner', 'loosers', 'children'] - def __init__(self, winner_time, looser_time, winner, loosers): - self.winner_time = winner_time - self.looser_time = looser_time - self.winner = winner - self.loosers = loosers - self.children = [] - - def checkCandidate(self): - if self.winner != None and self.winner.collision != None and self.winner.collision.looser_time < self.winner_time: - return False - for looser in self.loosers: - if looser.collision != None: - return False - return True - - def collide(self, lightcycles, collision_candidates, polygon_vertices, polygon_normal, tollerance=0.0001): - for looser in self.loosers: - looser.collision = self - if len(self.loosers) == 2: - assert(self.loosers[0].ground_normal@self.loosers[1].ground_normal > 0.0) - position = self.loosers[0].ground_origin+self.loosers[0].ground_velocity*self.looser_time - dirA = self.loosers[0].ground_velocity.normalized() - dirB = self.loosers[1].ground_velocity.normalized() - ground_dir = dirA+dirB - if ground_dir.length > tollerance: - index = 1 if self.loosers[0].slab_intersection.prev_slab.isOuterOfCollision(dirA, ground_dir, polygon_normal) else 0 - if dirA.cross(dirB)@polygon_normal > 0.0: - index = 1-index - self.children = [Lightcycle( - lightcycles, collision_candidates, polygon_vertices, polygon_normal, False, - self.looser_time, self.loosers[index].slab_intersection.prev_slab, self.loosers[1-index].slab_intersection.next_slab, - position, ground_dir.normalized(), self.loosers[0].ground_normal - )] - else: - ground_dir = dirA.cross(self.loosers[0].ground_normal) - index = 1 if self.loosers[0].slab_intersection.prev_slab.isOuterOfCollision(dirA, ground_dir, polygon_normal) else 0 - self.children = [Lightcycle( - lightcycles, collision_candidates, polygon_vertices, polygon_normal, False, - self.looser_time, self.loosers[index].slab_intersection.prev_slab, self.loosers[1-index].slab_intersection.next_slab, - position, ground_dir, self.loosers[0].ground_normal - ), Lightcycle( - lightcycles, collision_candidates, polygon_vertices, polygon_normal, True, - self.looser_time, self.loosers[1-index].slab_intersection.prev_slab, self.loosers[index].slab_intersection.next_slab, - position, -ground_dir, self.loosers[0].ground_normal - )] - -class Lightcycle: - __slots__ = ['start_time', 'ground_origin', 'ground_velocity', 'ground_normal', 'inwards', 'collision', 'slab_intersection'] - def __init__(self, lightcycles, collision_candidates, polygon_vertices, polygon_normal, immunity, start_time, prev_slab, next_slab, position, ground_dir, ground_normal): - exterior_angle = math.pi-math.acos(max(-1.0, min(prev_slab.edge@-next_slab.edge, 1.0))) - # pitch_angle = math.atan(math.cos(exterior_angle*0.5)) - ground_speed = 1.0/math.cos(exterior_angle*0.5) - self.start_time = start_time - self.ground_origin = position - self.ground_velocity = ground_dir*ground_speed - self.ground_normal = ground_normal - self.inwards = (self.ground_normal@polygon_normal > 0.0) - self.collision = None - self.slab_intersection = SlabIntersection(prev_slab, next_slab, None, None, 0.0, 0.0) - if self.inwards: - prev_slab.next_lightcycles.append(self) - next_slab.prev_lightcycles.append(self) - self.collideWithLightcycles(lightcycles, collision_candidates, immunity) - self.collideWithPolygon(collision_candidates, polygon_vertices, immunity) - lightcycles.append(self) - - def collideWithLightcycles(self, lightcycles, collision_candidates, immunity, arrival_tollerance=0.001): - for i in range(0, len(lightcycles)-1 if immunity == True else len(lightcycles)): - timeA, timeB = rayRayIntersection(self.ground_origin, self.ground_velocity, lightcycles[i].ground_origin, lightcycles[i].ground_velocity) - if math.isnan(timeA) or math.isnan(timeB): - continue - timeA += self.start_time - timeB += lightcycles[i].start_time - winner = None if abs(timeA-timeB) < arrival_tollerance else self if timeA < timeB else lightcycles[i] - # TODO: Insert in manyfold collision - insort_right(collision_candidates, lambda collision: collision.looser_time, Collision( - winner_time=min(timeA, timeB), - looser_time=max(timeA, timeB), - winner=winner, - loosers=([self, lightcycles[i]] if winner == None else [self if timeA > timeB else lightcycles[i]]) - )) - - def collideWithPolygon(self, collision_candidates, polygon_vertices, immunity): - min_time = float('inf') - for index in range(0, len(polygon_vertices)): - if type(immunity) is int and (index == immunity or index == (immunity+1)%len(polygon_vertices)): - continue - time = rayLineSegmentIntersection(self.ground_origin, self.ground_velocity, polygon_vertices[index-1], polygon_vertices[index]) - if not math.isnan(time): - min_time = min(time+self.start_time, min_time) - if min_time < float('inf'): - insort_right(collision_candidates, lambda collision: collision.looser_time, Collision( - winner_time=0.0, - looser_time=min_time, - winner=None, - loosers=[self] - )) - - def calculateSlabIntersection(self, tollerance=0.0001): - if self.collision == None: - return - self.slab_intersection.origin = self.ground_origin+self.ground_normal*self.start_time - dir = self.ground_velocity+self.ground_normal - self.slab_intersection.dir = dir.normalized() - self.slab_intersection.end_param = dir@self.slab_intersection.dir*(self.collision.looser_time-self.start_time) - if self.inwards: - self.slab_intersection.prev_slab.slab_intersections.append(self.slab_intersection) - self.slab_intersection.next_slab.slab_intersections.append(self.slab_intersection) - -def straightSkeletonOfPolygon(polygon_vertices, mesh_data, height=1.5, tollerance=0.0001): - polygon_normal = normalOfPolygon(polygon_vertices).normalized() - polygon_plane = Plane(normal=polygon_normal, distance=polygon_vertices[0]@polygon_normal) - for polygon_vertex in polygon_vertices: - if abs(polygon_vertex@polygon_plane.normal-polygon_plane.distance) > tollerance: - return 'Polygon is not planar / level' - - polygon_tangent = (polygon_vertices[1]-polygon_vertices[0]).normalized() - plane_matrix = Matrix.Identity(4) - plane_matrix.col[0] = polygon_tangent.to_4d() - plane_matrix.col[1] = polygon_normal.cross(polygon_tangent).normalized().to_4d() - plane_matrix.col[2] = polygon_normal.to_4d() - plane_matrix.col[3] = (polygon_plane.normal*polygon_plane.distance).to_4d() - plane_matrix.col[0].w = plane_matrix.col[1].w = plane_matrix.col[2].w = 0.0 - plane_matrix_inverse = plane_matrix.inverted() - plane_matrix_inverse.row[2].zero() - polygon_vertices = [plane_matrix_inverse@vertex for vertex in polygon_vertices] - polygon_normal = Vector((0.0, 0.0, 1.0)) - - slabs = [] - lightcycles = [] - collision_candidates = [] - - for index, next_polygon_vertex in enumerate(polygon_vertices): - prev_polygon_vertex = polygon_vertices[index-1] - slabs.append(Slab(polygon_normal, prev_polygon_vertex, next_polygon_vertex)) - - for index, prev_slab in enumerate(slabs): - next_slab = slabs[(index+1)%len(polygon_vertices)] - next_slab.prev_slab = prev_slab - prev_slab.next_slab = next_slab - Lightcycle( - lightcycles, collision_candidates, polygon_vertices, polygon_normal, index, - 0.0, prev_slab, next_slab, polygon_vertices[index], - (prev_slab.edge-next_slab.edge).normalized(), prev_slab.edge.cross(-next_slab.edge).normalized() - ) - - i = 0 - while i < len(collision_candidates): - collision = collision_candidates[i] - if collision.checkCandidate(): - collision.collide(lightcycles, collision_candidates, polygon_vertices, polygon_normal) - if len(collision.loosers) > 2: - return 'Manyfold collision' # TODO - i += 1 - - verts = [] - edges = [] - faces = [] - - for lightcycle in lightcycles: - lightcycle.calculateSlabIntersection() - # if lightcycle.collision != None: - # verts += [lightcycle.slab_intersection.origin, lightcycle.slab_intersection.origin+lightcycle.slab_intersection.dir*lightcycle.slab_intersection.end_param] - # edges.append((len(verts)-2, len(verts)-1)) - - for j, slabA in enumerate(slabs): - slabA.calculateVerticesFromLightcycles() - for i, slabB in enumerate(slabs): - if i >= j: - continue - slabA.calculateSlabIntersection(slabB, i == 0) - # for slab_intersection in slabA.slab_intersections: - # verts += [slab_intersection.origin+slab_intersection.dir*slab_intersection.begin_param, slab_intersection.origin+slab_intersection.dir*slab_intersection.end_param] - # edges.append((len(verts)-2, len(verts)-1)) - - for index, slab in enumerate(slabs): - slab.calculateVerticesFromIntersections() - vert_index = len(verts) - verts += slab.vertices - faces.append(range(vert_index, len(verts))) - - #mesh_data.from_pydata(verts, edges, faces) - return plane_matrix, verts, edges, faces - - - -def sliceMesh(src_mesh, dst_obj, distances, axis): - if dst_obj.type == 'MESH': - dst_obj.data.clear_geometry() - else: - dst_obj.data.splines.clear() - out_vertices = [] - out_edges = [] - for distance in distances: - aux_mesh = src_mesh.copy() - cut_geometry = bmesh.ops.bisect_plane(aux_mesh, geom=aux_mesh.edges[:]+aux_mesh.faces[:], dist=0, plane_co=axis*distance, plane_no=axis, clear_outer=False, clear_inner=False)['geom_cut'] - edge_pool = set((e for e in cut_geometry if isinstance(e, bmesh.types.BMEdge))) - while len(edge_pool) > 0: - current_edge = edge_pool.pop() - first_vertex = current_vertex = current_edge.verts[0] - vertices = [current_vertex.co] - while True: - current_vertex = current_edge.other_vert(current_vertex) - if current_vertex == first_vertex: - break - vertices.append(current_vertex.co) - follow_edge_loop = False - for edge in current_vertex.link_edges: - if edge in edge_pool: - current_edge = edge - edge_pool.remove(current_edge) - follow_edge_loop = True - break - if not follow_edge_loop: - break - if dst_obj.type == 'MESH': - for i in range(len(out_vertices), len(out_vertices)+len(vertices)-1): - out_edges.append((i, i+1)) - if current_vertex == first_vertex: - out_edges.append((len(out_vertices), len(out_vertices)+len(vertices)-1)) - out_vertices += [Vector(vertex) for vertex in vertices] - else: - addPolygonSpline(dst_obj, current_vertex == first_vertex, vertices) - aux_mesh.free() - if dst_obj.type == 'MESH': - dst_obj.data.from_pydata(out_vertices, out_edges, []) - -## /internal.py ######################################### - -class SvStraightSkeleton2DTest001(ModifierLiteNode, SverchCustomTreeNode, bpy.types.Node): - """ - Triggers: Merge two 2d meshes - - Each mesh can have disjoint parts - Only X and Y coordinate takes in account - """ - bl_idname = 'SvStraightSkeleton2DTest001' - bl_label = 'Straight Skeleton 2D Test001' - bl_icon = 'AUTOMERGE_ON' - - def draw_buttons_ext(self, context, layout): - col = layout.column(align=True) - pass - - def sv_init(self, context): - self.inputs.new('SvVerticesSocket', 'vertices') - self.inputs.new('SvStringsSocket' , 'edges') - self.inputs.new('SvStringsSocket' , 'polygons') - #self.inputs.new('SvMatrixSocket' , "matrixes") - - self.inputs['vertices'].label = 'Vertices' - self.inputs['edges'].label = 'Edges' - self.inputs['polygons'].label = 'Polygons' - #self.inputs['matrixes'].label = 'Matrixes' - - self.outputs.new('SvVerticesSocket', 'vertices') - self.outputs.new('SvStringsSocket' , 'edges') - self.outputs.new('SvStringsSocket' , 'polygons') - - self.outputs['vertices'].label = 'Vertices' - self.outputs['edges'].label = 'Edges' - self.outputs['polygons'].label = 'Polygons' - - def process(self): - if not all([sock.is_linked for sock in self.inputs]): - return - if not any([sock.is_linked for sock in self.outputs]): - return - - inputs = self.inputs - _Vertices = inputs['vertices'].sv_get(default=[[]], deepcopy=False) - Vertices = ensure_nesting_level(_Vertices, 3) - _Edges = inputs['edges'].sv_get(default=[[]], deepcopy=False) - Edges = ensure_nesting_level(_Edges, 3) - _Faces = inputs['polygons'].sv_get(default=[[]], deepcopy=False) - Faces = ensure_nesting_level(_Faces, 3) - - res_verts = [] - res_edges = [] - res_faces = [] - - for verts_i, edges_i, faces_i in zip_long_repeat(Vertices, Edges, Faces): - - #src_obj = bpy.context.object - vverts_i = [Vector(v) for v in verts_i] - polygons = [vverts_i] #selectedPolygons(src_obj) - if len(faces_i) != 1: - self.report({'WARNING'}, 'Invalid selection') - return {'CANCELLED'} - #dst_obj = addObject('MESH', 'Straight Skeleton') - plane_matrix, verts, edges, faces = straightSkeletonOfPolygon(polygons[0], None) - if isinstance(plane_matrix, str): - self.report({'WARNING'}, result) - return {'CANCELLED'} - #dst_obj.matrix_world = src_obj.matrix_world@plane_matrix - res_verts.append(verts) - res_edges.append(edges) - res_faces.append(faces) - pass - - self.outputs['vertices'].sv_set(res_verts) - self.outputs['edges'].sv_set(res_edges) - self.outputs['polygons'].sv_set(res_faces) - - pass - -classes = [SvStraightSkeleton2DTest001,] -register, unregister = bpy.utils.register_classes_factory(classes) \ No newline at end of file diff --git a/nodes/CAD/stright_skeleton_2d.py b/nodes/CAD/stright_skeleton_2d.py index b08cee7dcf..becc6b375f 100644 --- a/nodes/CAD/stright_skeleton_2d.py +++ b/nodes/CAD/stright_skeleton_2d.py @@ -13,23 +13,31 @@ import itertools import numpy as np import bpy, math, bmesh -from bpy.props import FloatProperty, BoolProperty, IntProperty +from bpy.props import FloatProperty, BoolProperty, IntProperty, EnumProperty from mathutils import Vector, Matrix -from more_itertools import sort_together from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode from sverchok.utils.geom_2d.merge_mesh import merge_mesh from sverchok.utils.nodes_mixins.sockets_config import ModifierLiteNode from sverchok.data_structure import dataCorrect, updateNode, zip_long_repeat, ensure_nesting_level, flatten_data -#from sverchok.utils.mesh.separate_loose_mesh import separate_loose_mesh +from sverchok.ui.sv_icons import custom_icon from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata from sverchok.nodes.analyzer.mesh_filter import Edges from sverchok.nodes.vector.vertices_sort import sort_vertices_by_connexions from sverchok.utils.modules.polygon_utils import areas_from_polygons from sverchok.utils.sv_operator_mixins import SvGenericNodeLocator -from pySVCGAL.pySVCGAL import pySVCGAL_extrude_skeleton +enable_module = False +try: + from more_itertools import sort_together + import pySVCGAL + from pySVCGAL.pySVCGAL import pySVCGAL_extrude_skeleton + enable_module = True +except ModuleNotFoundError: + enable_module = False + + def vertices_sort_by_edges(verts_in, edges_in): edges_indexes = list(itertools.chain(*edges_in)) @@ -45,19 +53,20 @@ def vertices_sort_by_edges(verts_in, edges_in): v0_idx = edges_indexes_0[pos] chain.append(v0_idx) + # Build Cchain to the right while True: v1_idx = edges_indexes_1[pos] if v1_idx in chain: - # цикл прервать, кольцо + # Break circle break chain.append(v1_idx) if v1_idx in edges_indexes_0: pos = edges_indexes_0.index(v1_idx) - #v0_idx = edges_indexes_0[pos] else: - # конец цепочки + # End of edjes break + # Build Chain to the left # Попробовать построить цепочку в обратном направлении (тут не в курсе, вышли из-за кольца # или что достигнут конец цепочки: @@ -69,13 +78,13 @@ def vertices_sort_by_edges(verts_in, edges_in): while True: v0_idx = edges_indexes_0[pos] if v0_idx in chain: - # цикл прервать, кольцо + # Circle break chain.append(v0_idx) if v0_idx in edges_indexes_1: pos = edges_indexes_1.index(v0_idx) - #v1_idx = edges_indexes_1[pos] else: + # End of circle # конец цепочки break @@ -154,6 +163,7 @@ def draw(self, context): bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) +# Operator to save data in .dat format file for test in CGAL (Hidden in production) class SvSaveCGALDatFile(bpy.types.Operator, SvGenericNodeLocator): ''' Save coords and angles to the file .dat for CGAL ''' bl_idname = "node.sverchok_save_cgal_dat_file" @@ -179,6 +189,8 @@ class SvStraightSkeleton2D(ModifierLiteNode, SverchCustomTreeNode, bpy.types.Nod bl_label = 'Straight Skeleton 2D' bl_icon = 'MOD_OUTLINE' + sv_dependencies = ['pySVCGAL', 'more_itertools'] + def wrapper_tracked_ui_draw_op(self, layout_element, operator_idname, **keywords): """ this wrapper allows you to track the origin of a clicked operator, by automatically passing @@ -195,31 +207,81 @@ def wrapper_tracked_ui_draw_op(self, layout_element, operator_idname, **keywords op.tree_name = self.id_data.name return op - ss_angle: FloatProperty( - default=0.785398, name="a float", update=updateNode, - description = "Angle of cliff", + ss_angles: FloatProperty( + default=0.785398, name="Taper angle", update=updateNode, + description = "Taper angle", subtype='ANGLE', ) # type: ignore ss_height: FloatProperty( - default=1.0, name="a float", update=updateNode, + default=1.0, name="Height", update=updateNode, description = "Max height", - #subtype='ANGLE', ) # type: ignore - merge_meshes: BoolProperty( - name='Merge', - description='Apply modifier geometry to import (original untouched)', - default=True, update=updateNode) # type: ignore + exclude_height: BoolProperty( + name="Exclude height", + description='Exclude height from calculations. (If you do not want change height to unlimit height)', + default=False, update=updateNode) # type: ignore + + only_tests_for_valid: BoolProperty( + name="Only tests", + description='Test all shapes are valid (safe time before start skeleton if meshes are highpoly)', + default=False, update=updateNode) # type: ignore verbose_messages_while_process: BoolProperty( name='Verbose', description='Show additional debug info in console', default=True, update=updateNode) # type: ignore + join_modes = [ + ('SPLIT', "Split", "Separate the result meshes into individual meshes", custom_icon("SV_VOM_SEPARATE_ALL_MESHES"), 0), + ('KEEP' , "Keep", "Keep as source meshes", custom_icon("SV_VOM_KEEP_SOURCE_MESHES"), 1), + ('MERGE', "Merge", "Join all results meshes into a single mesh", custom_icon("SV_VOM_JOIN_ALL_MESHES"), 2) + ] + + join_mode : EnumProperty( + name = "Output mode", + items = join_modes, + default = 'KEEP', + update = updateNode) # type: ignore + + objects_mask_modes = [ + ('BOOLEANS', "Booleans", "Boolean values (0/1) as mask of Voronoi Sites per objects [[0,1,0,0,1,1],[1,1,0,0,1],...]. Has no influence if socket is not connected (All sites are used)", 0), + ('INDEXES', "Indexes", "Indexes as mask of Voronoi Sites per objects [[1,2,0,4],[0,1,4,5,7],..]. Has no influence if socket is not connected (All sites are used)", 1), + ] + objects_mask_mode : EnumProperty( + name = "Mask of Objects", + items = objects_mask_modes, + default = 'BOOLEANS', + update = updateNode + ) # type: ignore + objects_mask_inversion : BoolProperty( + name = "Invert", + default = False, + description="Invert mask of sites. Has no influence if socket is not connected (All sites are used)", + update = updateNode) # type: ignore + + angles_modes = [ + ('OBJECTS', "Objects", "Use this angle on all edges of one objects", 'OBJECT_DATA', 0), + ('EDGES', "Edges", "Use many angles for objects", 'MOD_EDGESPLIT', 1), + ] + angles_mode : EnumProperty( + name = "Mask of Objects", + items = angles_modes, + default = 'OBJECTS', + update = updateNode + ) # type: ignore + + + def draw_vertices_out_socket(self, socket, context, layout): + layout.prop(self, 'join_mode', text='') + if socket.is_linked: # linked INPUT or OUTPUT + layout.label(text=f"{socket.label}. {socket.objects_number or ''}") + else: + layout.label(text=f'{socket.label}') + pass def draw_failed_contours_vertices_out_socket(self, socket, context, layout): - #layout.prop(self, 'enable_verts_attribute_sockets', icon='STICKY_UVS_DISABLE', text='', toggle=True) if socket.objects_number>0: layout.label(text=f'', icon='ERROR') layout.label(text=f'{socket.label} ') @@ -228,16 +290,49 @@ def draw_failed_contours_vertices_out_socket(self, socket, context, layout): elif socket.is_output: # unlinked OUTPUT layout.separator() + def updateMaskMode(self, context): + if self.objects_mask_mode=='BOOLEANS': + self.inputs["objects_mask"].label = "Mask of Objects" + elif self.objects_mask_mode=='INDEXES': + self.inputs["objects_mask"].label = "Indexes of Objects" + updateNode(self, context) + + def draw_objects_mask_in_socket(self, socket, context, layout): + grid = layout.grid_flow(row_major=True, columns=2) + if not socket.is_linked: + grid.enabled = False + col2 = grid.column() + col2_row1 = col2.row() + col2_row1.alignment='LEFT' + if socket.is_linked: + col2_row1.label(text=f"Mask of Objects. {socket.objects_number or ''}:") + else: + col2_row1.label(text=f"Mask of Objects:") + col2_row2 = col2.row() + col2_row2.alignment='LEFT' + col2_row2.column(align=True).prop(self, "objects_mask_inversion") + col3 = grid.column() + col3.prop(self, "objects_mask_mode", expand=True) + + def draw_angles_mode_in_socket(self, socket, context, layout): + grid = layout.grid_flow(row_major=False, columns=3, align=True) + col = grid.column() + col.prop(self, 'ss_angles') + if socket.is_linked==True: + col.enabled = False + else: + col.enabled = True + # Временно отключено + #grid.prop(self, 'angles_mode', expand=True, icon_only=True) def draw_buttons(self, context, layout): col = layout.column() - col.prop(self, 'ss_angle') - #col.prop(self, 'ss_height') - col.prop(self, 'merge_meshes') - col.prop(self, 'verbose_messages_while_process') - ui_file_save_dat = col.row() - self.wrapper_tracked_ui_draw_op(ui_file_save_dat, SvSaveCGALDatFile.bl_idname, text='', icon='DISK_DRIVE') - + col.prop(self, 'exclude_height') + col.prop(self, 'only_tests_for_valid') + col.prop(self, 'verbose_messages_while_process') + #col.row().prop(self, 'join_mode', expand=True) + #ui_file_save_dat = col.row() + #self.wrapper_tracked_ui_draw_op(ui_file_save_dat, SvSaveCGALDatFile.bl_idname, text='', icon='DISK_DRIVE') pass @@ -246,34 +341,36 @@ def draw_buttons_ext(self, context, layout): pass def sv_init(self, context): + + self.width = 180 + self.inputs.new('SvVerticesSocket', 'vertices') self.inputs.new('SvStringsSocket' , 'edges') self.inputs.new('SvStringsSocket' , 'polygons') - self.inputs.new('SvStringsSocket' , 'ssangles').prop_name = 'ss_angle' - self.inputs.new('SvStringsSocket' , 'ssheight').prop_name = 'ss_height' + self.inputs.new('SvStringsSocket' , 'ss_angles').prop_name = 'ss_angles' + self.inputs.new('SvStringsSocket' , 'ss_height').prop_name = 'ss_height' + self.inputs.new('SvStringsSocket' , 'objects_mask').label = "Mask of Objects" self.inputs.new('SvTextSocket' , 'file_name') - #self.inputs.new('SvMatrixSocket' , "matrixes") self.inputs['vertices'].label = 'Vertices' self.inputs['edges'].label = 'Edges' self.inputs['polygons'].label = 'Polygons' - self.inputs['ssangles'].label = 'Angles' - self.inputs['ssheight'].label = 'Height' + self.inputs['ss_angles'].label = 'Angles' + self.inputs['ss_angles'].custom_draw = 'draw_angles_mode_in_socket' + self.inputs['ss_height'].label = 'Height' + self.inputs['objects_mask'].custom_draw = 'draw_objects_mask_in_socket' self.inputs['file_name'].label = 'File Name' - #self.inputs['matrixes'].label = 'Matrixes' + self.inputs['file_name'].hide = True self.outputs.new('SvVerticesSocket', 'vertices') self.outputs.new('SvStringsSocket' , 'edges') self.outputs.new('SvStringsSocket' , 'polygons') - self.outputs.new('SvVerticesSocket', 'boundaries') - self.outputs.new('SvVerticesSocket', 'objects_boundaries_vertices') self.outputs.new('SvVerticesSocket', 'failed_contours_vertices') self.outputs['vertices'].label = 'Vertices' + self.outputs['vertices'].custom_draw = 'draw_vertices_out_socket' self.outputs['edges'].label = 'Edges' self.outputs['polygons'].label = 'Polygons' - self.outputs['boundaries'].label = 'Boundaries' - self.outputs['objects_boundaries_vertices'].label = 'Object Boud.Verts.' self.outputs['failed_contours_vertices'].label = 'Wrong contours verts' self.outputs['failed_contours_vertices'].custom_draw = 'draw_failed_contours_vertices_out_socket' @@ -283,7 +380,6 @@ def process(self): if not any([sock.is_linked for sock in self.outputs]): return - #print(f"== stright skeleton node start ===============================") inputs = self.inputs _Vertices = inputs['vertices'].sv_get(default=[[]], deepcopy=False) Vertices3 = ensure_nesting_level(_Vertices, 3) @@ -291,11 +387,18 @@ def process(self): Edges3 = ensure_nesting_level(_Edges, 3) _Faces = inputs['polygons'].sv_get(default=[[]], deepcopy=False) Faces3 = ensure_nesting_level(_Faces, 3) - _ssangles = inputs['ssangles'].sv_get(default=[[self.ss_angle]], deepcopy=False) - ssangles3 = ensure_nesting_level(_ssangles, 3) - _ssheights = inputs['ssheight'].sv_get(default=[[self.ss_height]], deepcopy=False) - ssheights3 = ensure_nesting_level(_ssheights, 3) + _ss_angles = inputs['ss_angles'].sv_get(default=[[self.ss_angles]], deepcopy=False) + if self.angles_mode=='OBJECTS': + ss_angles2 = ensure_nesting_level(_ss_angles, 2) + else: + ss_angles3 = ensure_nesting_level(_ss_angles, 3) + + _ss_heights = inputs['ss_height'].sv_get(default=[[self.ss_height]], deepcopy=False) + ss_heights1 = ensure_nesting_level(_ss_heights, 2) + _objects_mask_in = inputs['objects_mask'].sv_get(default=[[]], deepcopy=False) + objects_mask_in = ensure_nesting_level(_objects_mask_in, 2)[0] + _file_names = inputs['file_name'].sv_get(default=[[]], deepcopy=False) file_names3 = ensure_nesting_level(_file_names, 3) file_name_dat = None @@ -313,87 +416,132 @@ def process(self): objects_area_boundaries = [] contours_failed_at_all = [] + params = zip_long_repeat(Vertices3, Edges3, Faces3) + + len_vertices3 = len(Vertices3) + np_mask = np.zeros(len_vertices3, dtype=bool) + if self.inputs['objects_mask'].is_linked: + if self.objects_mask_mode=='BOOLEANS': + for I in range(len_vertices3): + if I0: - lst_errors1.append(f"Failed polygon_id: {polygon_id}. ") + lst_errors1.append(f"Polygon : {polygon_id} is failed. ") if data1['str_error']: lst_errors1[-1] = lst_errors1[-1] + (data1['str_error']) for s in data1['ftcs_vertices_description']: @@ -403,59 +551,84 @@ def parallel_extrude_skeleton(data1): pass else: #print(f"\nPolygon_id: {polygon_id} is good. It has no errors") - if self.merge_meshes==True: - object_verts.extend(data1['vertices']) - object_faces.extend( [ list(map(lambda n: n+faces_delta, face)) for face in data1['faces'] ] ) - faces_delta+=len(data1['vertices']) + if self.only_tests_for_valid==True: + # no result, no output + pass else: - res_verts.append( data1['vertices'] ) - res_faces.append( data1['faces'] ) + if self.join_mode=='SPLIT': + res_verts.append( data1['vertices'] ) + res_edges.append( data1['edges'] ) + res_faces.append( data1['faces'] ) + pass + elif self.join_mode=='KEEP': + object_id = data1['object_id'] + if object_id not in objects_verts_keep: + objects_verts_keep[object_id] = [] + objects_edges_keep[object_id] = [] + objects_faces_keep[object_id] = [] + objects_faces_keep_delta[object_id] = 0 + pass + faces_delta_id = objects_faces_keep_delta[object_id] + objects_verts_keep[object_id].extend(data1['vertices']) + objects_edges_keep[object_id].extend( [ list(map(lambda n: n+faces_delta_id, face)) for face in data1['edges'] ] ) + objects_faces_keep[object_id].extend( [ list(map(lambda n: n+faces_delta_id, face)) for face in data1['faces'] ] ) + objects_faces_keep_delta[object_id]+=len(data1['vertices']) + pass + elif self.join_mode=='MERGE': + object_verts_merge.extend(data1['vertices']) + object_edges_merge.extend( [ list(map(lambda n: n+faces_delta, face)) for face in data1['edges'] ] ) + object_faces_merge.extend( [ list(map(lambda n: n+faces_delta, face)) for face in data1['faces'] ] ) + faces_delta+=len(data1['vertices']) + pass + + len_verts1 = len(data1['vertices']) + len_edges1 = len(data1['edges']) + len_faces1 = len(data1['faces']) + idx = data1['polygon_id'] + str_error = f'Polygon {idx} is good. Stright Skeleton mesh: verts {len_verts1}, edges {len_edges1}, faces {len_faces1}' + lst_errors1.append(str_error) pass lst_errors.extend(lst_errors1) - if self.merge_meshes==True: - res_verts.append(object_verts) - res_faces.append(object_faces) - # else: - # res_verts.extend(object_verts) - # res_faces.extend(object_faces) + if self.join_mode=='MERGE': + res_verts.append(object_verts_merge) + res_edges.append(object_edges_merge) + res_faces.append(object_faces_merge) + elif self.join_mode=='KEEP': + for KEY in objects_verts_keep: + res_verts.append(objects_verts_keep[KEY]) + res_edges.append(objects_edges_keep[KEY]) + res_faces.append(objects_faces_keep[KEY]) + else: + pass if len(contours_failed_at_all)>0: failed_contours_vertices.extend(contours_failed_at_all) pass - print("") - print("Node Skeleton Finished.") - if lst_errors: - for s in lst_errors: - print(s) + if was_errors: + print("") + print("") + print("Node Skeleton Finished with errors.") if self.verbose_messages_while_process==False: - print("") print("for more info turn on verbose mode in node") + else: + for s in lst_errors: + print(s) + + else: + print("\nNode Skeleton Finished.") else: # file_name_dat: - # faces_delta = 0 - # object_verts = [] - # object_faces = [] - # for I in range(len(objects_boundaries)): - # #print(f"== stright skeleton {I} ===============================") - # objects_boundaries_I = objects_boundaries[I] - # objects_angles_of_boundaries_I = objects_angles_of_boundaries[I] - # new_mesh = pySVCGAL_extrude_skeleton(self.ss_height/1.0, objects_boundaries_I, objects_angles_of_boundaries_I, True) - # object_verts.extend(new_mesh['vertices']) - # object_faces.extend( [ list(map(lambda n: n+faces_delta, face)) for face in new_mesh['faces'] ] ) - # faces_delta+=len(new_mesh['vertices']) - # # res_verts.append(new_mesh['vertices']) - # # res_faces.append(new_mesh['faces']) - # pass - # res_verts.append(object_verts) - # res_faces.append(object_faces) + # for DEVELOPERS: lines_verts = [] lines_angles = [] + # for .dat format save only vertices of first object. # Записывать вершины только первого объекта, т.к. только один объект и может быть рассчитал в CGAL # Когда сделаю компонент, то тогда передам все объекты по очереди. objects_boundaries_0 = objects_boundaries[0] + objects_angles_of_boundaries_0 = objects_angles_of_boundaries[0][0] for I in range(len(objects_boundaries_0)): objects_boundaries_0_I = objects_boundaries_0[I] lines_verts .append(str(len(objects_boundaries_0_I)),) @@ -468,6 +641,7 @@ def parallel_extrude_skeleton(data1): v_str = [str(v) for v in vert[:2] ] v_line = " ".join(v_str) lines_verts.append(v_line) + for angle in objects_angles_of_boundaries_0: lines_angles.append( str(self.ss_angle*180/math.pi) ) txt_verts = "\n".join(lines_verts) txt_angles = "\n".join(lines_angles) @@ -483,15 +657,13 @@ def parallel_extrude_skeleton(data1): self.outputs['vertices'].sv_set(res_verts) self.outputs['edges'].sv_set(res_edges) self.outputs['polygons'].sv_set(res_faces) - self.outputs['boundaries'].sv_set(res_boundaries_verts) - self.outputs['objects_boundaries_vertices'].sv_set(objects_boundaries) self.outputs['failed_contours_vertices'].sv_set(failed_contours_vertices) pass def saveCGALDatFile(self): - if not all([sock.is_linked for sock in self.inputs if sock.name not in ['ssangles', 'file_name'] ]): - return 'Не подключены сокеты Vertices и Faces. Файлы не записаны.' + if not all([sock.is_linked for sock in self.inputs if sock.name not in ['ss_angles', 'file_name'] ]): + return 'Vertices и Faces not connected. Files are not saved.' print("file .dat saved") pass diff --git a/settings.py b/settings.py index 6f8e5339b6..e50fd601f5 100644 --- a/settings.py +++ b/settings.py @@ -623,6 +623,7 @@ def draw_freecad_ops(): draw_message(box, "ezdxf") draw_message(box, "pyacvd") draw_message(box, "pyQuadriFlow") + draw_message(box, "pySVCGAL") draw_freecad_ops() From d1d30170f22b23043765334cf1cf66e3ea405591 Mon Sep 17 00:00:00 2001 From: satabol Date: Sat, 12 Oct 2024 00:59:24 +0300 Subject: [PATCH 3/7] - update docs/nodes/CAD/stright_skeleton_2d.rst --- docs/nodes/CAD/stright_skeleton_2d.rst | 43 +++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/nodes/CAD/stright_skeleton_2d.rst b/docs/nodes/CAD/stright_skeleton_2d.rst index 48a6e0fc8b..bcbf02b4bf 100644 --- a/docs/nodes/CAD/stright_skeleton_2d.rst +++ b/docs/nodes/CAD/stright_skeleton_2d.rst @@ -22,14 +22,24 @@ subtending the edges, tracing a tree-like structure, the straight skeleton. .. image:: https://github.com/user-attachments/assets/8baaadf0-1e09-454a-af6a-85517db3bdb4 :target: https://github.com/user-attachments/assets/50fd85bb-db65-41d3-a536-142c2cefffac +Install dependency +------------------ + +To use node install additional library pySVCGAL in the Extra Nodes Section: + +.. image:: https://github.com/user-attachments/assets/548ad0a2-86af-4f12-9f39-230f6cda7d41 + :target: https://github.com/user-attachments/assets/548ad0a2-86af-4f12-9f39-230f6cda7d41 + + + Inputs ------ -- Vertices, Edges, Faces - Input Mesh (2D only) +- **Vertices**, **Edges**, **Faces** - Input Mesh (2D only) .. image:: https://github.com/user-attachments/assets/3ce5a747-9b8d-4aef-94fc-9e268425e6a6 :target: https://github.com/user-attachments/assets/3ce5a747-9b8d-4aef-94fc-9e268425e6a6 -- Taper Angle - Angle between plane and Face that this algorithm will build. +- **Taper Angle** - Angle between plane and Face that this algorithm will build. .. image:: https://github.com/user-attachments/assets/666c3a16-f124-4230-906b-8b4ea2cd699c :target: https://github.com/user-attachments/assets/666c3a16-f124-4230-906b-8b4ea2cd699c @@ -54,7 +64,7 @@ If you connect list of floats then it will be used per objects: .. image:: https://github.com/user-attachments/assets/f40f4f4f-92dd-4d41-9eae-ddb890214fb6 :target: https://github.com/user-attachments/assets/f40f4f4f-92dd-4d41-9eae-ddb890214fb6 -- Height - Height of object or objects. If used single value then this value vill be used for every objects. If socket is connected with float values then values will be used per objects: +- **Height** - Height of object or objects. If used single value then this value vill be used for every objects. If socket is connected with float values then values will be used per objects: .. raw:: html @@ -65,7 +75,7 @@ If you connect list of floats then it will be used per objects: | -- Mask of objects - Mask hide objects. If element of boolean mask is True then object are hidden. If length of mask is more than length of objects then exceeded values will be omitted. +- **Mask of objects** - Mask hide objects. If element of boolean mask is True then object are hidden. If length of mask is more than length of objects then exceeded values will be omitted. .. raw:: html @@ -94,7 +104,7 @@ Parameters .. image:: https://github.com/user-attachments/assets/0119d5b9-09d2-49a4-b4fd-91e0afdcf76c :target: https://github.com/user-attachments/assets/0119d5b9-09d2-49a4-b4fd-91e0afdcf76c -- Join mode. Split, Keep, Merge. +- **Join mode**. **Split**, **Keep**, **Merge**. - **Split** - If some of objects has several independent meshes then they will be splitten individually and you can get more object on output than on input. (Mask will hide all meshes in multimesh objects) .. image:: https://github.com/user-attachments/assets/5d76cd4f-bb2a-4a05-b218-85ae0d96adee @@ -129,6 +139,11 @@ Parameters .. image:: https://github.com/user-attachments/assets/f71aba10-3d00-48d0-b352-907f20b45ef8 :target: https://github.com/user-attachments/assets/f71aba10-3d00-48d0-b352-907f20b45ef8 +Output sockets +-------------- + + + Performance ----------- @@ -176,3 +191,21 @@ Src: https://www.templatesarea.com/celtic-tree-of-life-silhouettes-free-vector-g .. image:: https://github.com/user-attachments/assets/6527588d-a89e-4b04-8965-9450014cc0ba :target: https://github.com/user-attachments/assets/6527588d-a89e-4b04-8965-9450014cc0ba + +Creating Abstract Shape from 2D Bezier Circle +--------------------------------------------- + +.. image:: https://github.com/user-attachments/assets/1feac759-2b7f-4266-86f4-f9e0a8e0244d + :target: https://github.com/user-attachments/assets/1feac759-2b7f-4266-86f4-f9e0a8e0244d + +.. raw:: html + + + +This shape with autosmooth: + +.. image:: https://github.com/user-attachments/assets/10c38207-9d24-4b00-bcd6-84d502bc964e + :target: https://github.com/user-attachments/assets/10c38207-9d24-4b00-bcd6-84d502bc964e \ No newline at end of file From 1ae867660c589f5417ee3df51cc6ea483c0e47d1 Mon Sep 17 00:00:00 2001 From: satabol Date: Sat, 12 Oct 2024 21:57:24 +0300 Subject: [PATCH 4/7] - parameter "Exclude height" renamed into "Restrict height" (Inner parameter stay unchanged, UI inverted) - docs/nodes/CAD/stright_skeleton_2d.rst updated --- docs/nodes/CAD/stright_skeleton_2d.rst | 6 ++++-- nodes/CAD/stright_skeleton_2d.py | 26 ++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/nodes/CAD/stright_skeleton_2d.rst b/docs/nodes/CAD/stright_skeleton_2d.rst index bcbf02b4bf..501fb03413 100644 --- a/docs/nodes/CAD/stright_skeleton_2d.rst +++ b/docs/nodes/CAD/stright_skeleton_2d.rst @@ -4,6 +4,8 @@ Stright Skeleton 2d .. image:: https://github.com/user-attachments/assets/2a141705-62e6-489b-a4be-9333df8c7951 :target: https://github.com/user-attachments/assets/2a141705-62e6-489b-a4be-9333df8c7951 +!!! **"Exclude height" parameter was renamed into "Restrict Height" !!! Images will be updated later...** + Functionality ------------- @@ -39,7 +41,7 @@ Inputs .. image:: https://github.com/user-attachments/assets/3ce5a747-9b8d-4aef-94fc-9e268425e6a6 :target: https://github.com/user-attachments/assets/3ce5a747-9b8d-4aef-94fc-9e268425e6a6 -- **Taper Angle** - Angle between plane and Face that this algorithm will build. +- **Taper Angle** - Angle between plane and Face that this algorithm will build. Valid range is 0 < Taper Angle <180 Degrees; 0 and 180 are invelid angles. Also 90 degrees is invalid param if "Restrict Height" is off. .. image:: https://github.com/user-attachments/assets/666c3a16-f124-4230-906b-8b4ea2cd699c :target: https://github.com/user-attachments/assets/666c3a16-f124-4230-906b-8b4ea2cd699c @@ -120,7 +122,7 @@ Parameters .. image:: https://github.com/user-attachments/assets/bd119bb8-ad08-4983-be67-d97c20ad8bb3 :target: https://github.com/user-attachments/assets/bd119bb8-ad08-4983-be67-d97c20ad8bb3 - - **Exclude Height** - If you want to see objects without height limits just turn it on. All objects will be recalulated without heights limits. + - **Restrict Height** (old name is "Exclude Height")- If you want to see objects without height limits just turn it off. All objects will be recalulated without heights limits (in the input field or socket). .. raw:: html diff --git a/nodes/CAD/stright_skeleton_2d.py b/nodes/CAD/stright_skeleton_2d.py index becc6b375f..cc8b394efe 100644 --- a/nodes/CAD/stright_skeleton_2d.py +++ b/nodes/CAD/stright_skeleton_2d.py @@ -215,12 +215,13 @@ def wrapper_tracked_ui_draw_op(self, layout_element, operator_idname, **keywords ss_height: FloatProperty( default=1.0, name="Height", update=updateNode, - description = "Max height", + description = "Max height. Disabled if (socket connected or Restrict height is off)", ) # type: ignore + # !!! Inverted in UI. Named as in a CGAL library exclude_height: BoolProperty( - name="Exclude height", - description='Exclude height from calculations. (If you do not want change height to unlimit height)', + name="Restrict height", + description='Restrict height of object. (If no then all heights wil bu unlimited height)', default=False, update=updateNode) # type: ignore only_tests_for_valid: BoolProperty( @@ -325,9 +326,25 @@ def draw_angles_mode_in_socket(self, socket, context, layout): # Временно отключено #grid.prop(self, 'angles_mode', expand=True, icon_only=True) + def draw_ss_height_in_socket(self, socket, context, layout): + grid = layout.grid_flow(row_major=False, columns=3, align=True) + col = grid.column() + col.prop(self, 'ss_height') + if socket.is_linked==True: + col.enabled = False + else: + if self.exclude_height==True: + col.enabled = False + else: + col.enabled = True + pass + #col.enabled = True + pass + pass + def draw_buttons(self, context, layout): col = layout.column() - col.prop(self, 'exclude_height') + col.prop(self, 'exclude_height', invert_checkbox=True) col.prop(self, 'only_tests_for_valid') col.prop(self, 'verbose_messages_while_process') #col.row().prop(self, 'join_mode', expand=True) @@ -358,6 +375,7 @@ def sv_init(self, context): self.inputs['ss_angles'].label = 'Angles' self.inputs['ss_angles'].custom_draw = 'draw_angles_mode_in_socket' self.inputs['ss_height'].label = 'Height' + self.inputs['ss_height'].custom_draw = 'draw_ss_height_in_socket' self.inputs['objects_mask'].custom_draw = 'draw_objects_mask_in_socket' self.inputs['file_name'].label = 'File Name' self.inputs['file_name'].hide = True From e3412e4364dbb2df67f7616f9bb1dfe8a605d6eb Mon Sep 17 00:00:00 2001 From: satabol Date: Sat, 12 Oct 2024 22:29:09 +0300 Subject: [PATCH 5/7] - fix exception on nonborder objects (like cube, sphere and other volume objects). Only flats allowed. --- nodes/CAD/stright_skeleton_2d.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodes/CAD/stright_skeleton_2d.py b/nodes/CAD/stright_skeleton_2d.py index cc8b394efe..0b951b8f83 100644 --- a/nodes/CAD/stright_skeleton_2d.py +++ b/nodes/CAD/stright_skeleton_2d.py @@ -493,6 +493,8 @@ def process(self): contours_failed_at_all.append(verts_i_separated_IJ) continue + if not BoundaryEdges: + raise Exception(f"Error: Object {I} has no boundaries. Extrusion is not possible. Objects should be flat.") # separate contours of every island verts_boundaries, edges_boundaries, _ = separate_loose_mesh(verts_i_separated_IJ, BoundaryEdges) From 92e5333aa7d376a0cff53d8e45fa47a47835d9df Mon Sep 17 00:00:00 2001 From: satabol Date: Sat, 12 Oct 2024 23:01:52 +0300 Subject: [PATCH 6/7] - added to the node name "Alpha". --- docs/nodes/CAD/stright_skeleton_2d.rst | 8 +++++--- nodes/CAD/stright_skeleton_2d.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/nodes/CAD/stright_skeleton_2d.rst b/docs/nodes/CAD/stright_skeleton_2d.rst index 501fb03413..53f41166b1 100644 --- a/docs/nodes/CAD/stright_skeleton_2d.rst +++ b/docs/nodes/CAD/stright_skeleton_2d.rst @@ -1,10 +1,12 @@ -Stright Skeleton 2d -=================== +Stright Skeleton 2d (Alpha) +=========================== .. image:: https://github.com/user-attachments/assets/2a141705-62e6-489b-a4be-9333df8c7951 :target: https://github.com/user-attachments/assets/2a141705-62e6-489b-a4be-9333df8c7951 -!!! **"Exclude height" parameter was renamed into "Restrict Height" !!! Images will be updated later...** +!!! **"Exclude height" parameter was renamed into "Restrict Height" !!! Images will be update later...** + +**It is Alpha version of node. Any function and names can be changed without notification! (but it will most likely be reported in the documentation).** Functionality ------------- diff --git a/nodes/CAD/stright_skeleton_2d.py b/nodes/CAD/stright_skeleton_2d.py index 0b951b8f83..3e59e7ed56 100644 --- a/nodes/CAD/stright_skeleton_2d.py +++ b/nodes/CAD/stright_skeleton_2d.py @@ -186,7 +186,7 @@ class SvStraightSkeleton2D(ModifierLiteNode, SverchCustomTreeNode, bpy.types.Nod Only X and Y coordinate takes in account """ bl_idname = 'SvStraightSkeleton2D' - bl_label = 'Straight Skeleton 2D' + bl_label = 'Straight Skeleton 2D (Alpha)' bl_icon = 'MOD_OUTLINE' sv_dependencies = ['pySVCGAL', 'more_itertools'] From dd8d161e2f473bbfa1ffd157d835997707ed2f3e Mon Sep 17 00:00:00 2001 From: satabol Date: Sat, 12 Oct 2024 23:30:20 +0300 Subject: [PATCH 7/7] - fix docs/nodes/CAD/stright_skeleton_2d.rst --- docs/nodes/CAD/stright_skeleton_2d.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/nodes/CAD/stright_skeleton_2d.rst b/docs/nodes/CAD/stright_skeleton_2d.rst index 53f41166b1..8960f24a4d 100644 --- a/docs/nodes/CAD/stright_skeleton_2d.rst +++ b/docs/nodes/CAD/stright_skeleton_2d.rst @@ -8,6 +8,8 @@ Stright Skeleton 2d (Alpha) **It is Alpha version of node. Any function and names can be changed without notification! (but it will most likely be reported in the documentation).** +Now tested with **Windows 10**, **Windows 11**, **Ubuntu 22.04** (minimal libstdc++ version 12.3 ). It is not work with iOS (Apple) and we has no plan for a while. If you get issue please write https://github.com/nortikin/sverchok/issues + Functionality -------------