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..8960f24a4d --- /dev/null +++ b/docs/nodes/CAD/stright_skeleton_2d.rst @@ -0,0 +1,217 @@ +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 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).** + +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 +------------- + +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 + +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) + .. 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. 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 + + 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 + + - **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 + + + + - **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 + +Output sockets +-------------- + + + + +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 + + +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 diff --git a/index.yaml b/index.yaml index 878bb3747f..97cec560bd 100644 --- a/index.yaml +++ b/index.yaml @@ -494,6 +494,8 @@ - SvCropMesh2D - SvCSGBooleanNodeMK2 - SvWafelNode + - --- + - SvStraightSkeleton2D - --- 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/stright_skeleton_2d.py b/nodes/CAD/stright_skeleton_2d.py new file mode 100644 index 0000000000..3e59e7ed56 --- /dev/null +++ b/nodes/CAD/stright_skeleton_2d.py @@ -0,0 +1,692 @@ +# 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, EnumProperty +from mathutils import Vector, Matrix + +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.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 + +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)) + 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) + + # 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) + else: + # End of edjes + break + + # Build Chain to the left + # Попробовать построить цепочку в обратном направлении (тут не в курсе, вышли из-за кольца + # или что достигнут конец цепочки: + + 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: + # Circle + break + chain.append(v0_idx) + if v0_idx in edges_indexes_1: + pos = edges_indexes_1.index(v0_idx) + else: + # End of circle + # конец цепочки + 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) + + +# 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" + 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 (Alpha)' + 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 + 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_angles: FloatProperty( + default=0.785398, name="Taper angle", update=updateNode, + description = "Taper angle", + subtype='ANGLE', + ) # type: ignore + + ss_height: FloatProperty( + default=1.0, name="Height", update=updateNode, + 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="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( + 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): + 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 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_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', 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) + #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.width = 180 + + self.inputs.new('SvVerticesSocket', 'vertices') + self.inputs.new('SvStringsSocket' , 'edges') + self.inputs.new('SvStringsSocket' , 'polygons') + 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['vertices'].label = 'Vertices' + self.inputs['edges'].label = 'Edges' + self.inputs['polygons'].label = 'Polygons' + 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 + + self.outputs.new('SvVerticesSocket', 'vertices') + self.outputs.new('SvStringsSocket' , 'edges') + self.outputs.new('SvStringsSocket' , 'polygons') + 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['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 + + 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) + _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 + 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 = [] + 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"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']: + 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.only_tests_for_valid==True: + # no result, no output + pass + else: + 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.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 + + if was_errors: + print("") + print("") + print("Node Skeleton Finished with errors.") + if self.verbose_messages_while_process==False: + 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: + + # 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)),) + 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) + 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) + + 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['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 ['ss_angles', 'file_name'] ]): + return 'Vertices и Faces not connected. Files are not saved.' + + print("file .dat saved") + pass + +classes = [SvSaveCGALDatFile, SvStraightSkeleton2D,] +register, unregister = bpy.utils.register_classes_factory(classes) \ No newline at end of file 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()