diff --git a/dependencies.py b/dependencies.py index 310d91b542..40a82672a1 100644 --- a/dependencies.py +++ b/dependencies.py @@ -193,6 +193,14 @@ def draw_message(box, package, dependencies=None): except ImportError: ezdxf = None +pyacvd_d = sv_dependencies["pyacvd"] = SvDependency("pyacvd","https://github.com/pyvista/pyacvd") +pyacvd_d.pip_installable = True +try: + import pyacvd + pyacvd_d.module = pyacvd +except ImportError: + pyacvd = None + settings.pip = pip settings.sv_dependencies = sv_dependencies settings.ensurepip = ensurepip diff --git a/docs/nodes/spatial/mesh_clustering.rst b/docs/nodes/spatial/mesh_clustering.rst new file mode 100644 index 0000000000..1574521ecc --- /dev/null +++ b/docs/nodes/spatial/mesh_clustering.rst @@ -0,0 +1,72 @@ +Mesh clustering +=============== + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/d850920c-856c-4beb-a273-a779d86ef434 + :target: https://github.com/nortikin/sverchok/assets/14288520/d850920c-856c-4beb-a273-a779d86ef434 + +Dependencies +------------ + +This node requires pyacvd_ library to work. One can install it in addon settings: + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/65467755-82b9-4e71-802d-0978eb2e849d + :target: https://github.com/nortikin/sverchok/assets/14288520/65467755-82b9-4e71-802d-0978eb2e849d + +Functionality +------------- + +This node takes a surface mesh and returns a uniformly meshed surface using voronoi clustering. + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/f50b985a-45b6-482c-8b7c-1f7551a50b8f + :target: https://github.com/nortikin/sverchok/assets/14288520/f50b985a-45b6-482c-8b7c-1f7551a50b8f + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/e761653b-a88f-42e1-b222-fa243b1642a5 + :target: https://github.com/nortikin/sverchok/assets/14288520/e761653b-a88f-42e1-b222-fa243b1642a5 + +Inputs +------ + +This node has the following inputs: + +- **Vertices**, **Edges**, **Faces** - Source mesh + +Parameters +---------- + +- **Subdivide** - if source mesh is not dense enough for uniform remeshing then subdivide source mesh. A linear subdivision of the mesh. If model has high dense one can low Subdivide param to zero. For low dense mesh a value 3 is good. + + .. image:: https://github.com/nortikin/sverchok/assets/14288520/2d2d6e3a-747e-4be2-88e2-50e9c9b39c1e + :target: https://github.com/nortikin/sverchok/assets/14288520/2d2d6e3a-747e-4be2-88e2-50e9c9b39c1e + +- **Max itereation** - Max iteration of clusterization. +- **Clusters** - Cluster counts. + + .. image:: https://github.com/nortikin/sverchok/assets/14288520/6d49a5cf-704d-4566-b56e-861a88b9d34f + :target: https://github.com/nortikin/sverchok/assets/14288520/6d49a5cf-704d-4566-b56e-861a88b9d34f + +- **Triangulate mesh polygons**. If your mesh has faces with different count of vertices then this mash has to be triangulated. + +Outputs +------- + +Triangulated mesh: + +- **Vertices**, **Edges**, **Faces** + + +Example of Usage +---------------- + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/4140d9d7-c710-4ad7-a223-5ca62f4069f1 + :target: https://github.com/nortikin/sverchok/assets/14288520/4140d9d7-c710-4ad7-a223-5ca62f4069f1 + +High poly clusterizing +---------------------- + +Subdivide is zero. + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/f4ca089c-7704-41aa-943f-b629f1dc10ab + :target: https://github.com/nortikin/sverchok/assets/14288520/f4ca089c-7704-41aa-943f-b629f1dc10ab + + +.. _pyacvd: https://github.com/pyvista/pyacvd \ No newline at end of file diff --git a/index.yaml b/index.yaml index 50b6ffce5f..446064215d 100644 --- a/index.yaml +++ b/index.yaml @@ -327,6 +327,7 @@ - --- - SvConvexHullNodeMK2 - SvConcaveHullNode + - SvMeshClusteringNode - Transforms: - icon_name: ORIENTATION_LOCAL diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index 38d1d84b93..95f6a0ea98 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -520,6 +520,7 @@ - --- - SvConvexHullNodeMK2 - SvConcaveHullNode + - SvMeshClusteringNode - --- diff --git a/menus/full_nortikin.yaml b/menus/full_nortikin.yaml index 52dfb16ffd..ba9823486d 100644 --- a/menus/full_nortikin.yaml +++ b/menus/full_nortikin.yaml @@ -395,6 +395,7 @@ - --- - SvConvexHullNodeMK2 - SvConcaveHullNode + - SvMeshClusteringNode ################################################################################ diff --git a/nodes/spatial/mesh_clustering.py b/nodes/spatial/mesh_clustering.py new file mode 100644 index 0000000000..7a071a7432 --- /dev/null +++ b/nodes/spatial/mesh_clustering.py @@ -0,0 +1,191 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +from itertools import product +import numpy as np +from sverchok.dependencies import pyacvd +if pyacvd is not None: + from pyvista import examples, PolyData +#import pyacvd + +import bpy +import bmesh +from bpy.props import BoolVectorProperty, EnumProperty, BoolProperty, FloatProperty, IntProperty +from mathutils import Matrix + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.utils.nodes_mixins.recursive_nodes import SvRecursiveNode +from sverchok.utils.sv_mesh_utils import mesh_join +from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh +from sverchok.utils.modules.matrix_utils import matrix_apply_np + +from sverchok.data_structure import dataCorrect, updateNode, zip_long_repeat +from sverchok.utils.geom import bounding_box_aligned + +class SvMeshClusteringNode(SverchCustomTreeNode, bpy.types.Node, SvRecursiveNode): + """ + Triggers: Bbox 2D or 3D + Tooltip: Get vertices bounding box (vertices, sizes, center) + """ + bl_idname = 'SvMeshClusteringNode' + bl_label = 'Mesh Clustering (Alpha)' + bl_icon = 'IMAGE_ALPHA' + sv_icon = 'SV_DELAUNAY' + sv_dependencies = {'pyacvd'} + + output_as_numpy : BoolProperty( + name = "Output as numpy", + description = "If checked then output sockets as numpy", + default = False, + update = updateNode) # type: ignore + + + quad_modes = [ + ( "BEAUTY", "Beauty", "Split the quads in nice triangles, slower method", 1), + ( "FIXED", "Fixed", "Split the quads on the 1st and 3rd vertices", 2), + ( "ALTERNATE", "Fixed Alternate", "Split the quads on the 2nd and 4th vertices", 3), + ("SHORT_EDGE", "Shortest Diagonal", "Split the quads based on the distance between the vertices", 4) + ] + + ngon_modes = [ + ( "BEAUTY", "Beauty", "Arrange the new triangles nicely, slower method", 1), + ("EAR_CLIP", "Clip", "Split the ngons using a scanfill algorithm", 2) + ] + + quad_mode: EnumProperty( + name='Quads mode', + description="Quads processing mode", + items=quad_modes, + default="BEAUTY", + update=updateNode) # type: ignore + + ngon_mode: EnumProperty( + name="Polygons mode", + description="Polygons processing mode", + items=ngon_modes, + default="BEAUTY", + update=updateNode) # type: ignore + + cluster_subdivide : IntProperty( + min=0, default=0, name='Subdivide', + description="Cluster subdivide", update=updateNode) # type: ignore + max_iter : IntProperty( + min=1, default=100, name='Max iteration', + description="Max iteration of clusterization", update=updateNode) # type: ignore + + cluster_counts : IntProperty( + min=0, default=1000, name='Clusters', + description="Cluster counts", update=updateNode) # type: ignore + + + def update_sockets(self, context): + updateNode(self, context) + + def draw_buttons_ext(self, context, layout): + layout.row().prop(self, 'output_as_numpy') + + def draw_buttons(self, context, layout): + layout.row().prop(self, 'cluster_subdivide') + layout.row().prop(self, 'max_iter') + layout.row().prop(self, 'cluster_counts') + + col = layout.column() + col.row().label(text="Triangulate mesh polygons:") + row = col.row() + split = row.split(factor=0.4) + split.column().label(text="Quads mode:") + split.column().row(align=True).prop(self, "quad_mode", text='') + + row = col.row() + split = row.split(factor=0.5) + split.column().label(text="Polygons mode:") + split.column().row(align=True).prop(self, "ngon_mode", text='') + + pass + + def sv_init(self, context): + son = self.outputs.new + self.inputs.new('SvVerticesSocket', 'Vertices') + self.inputs.new('SvStringsSocket', 'Edges') + self.inputs.new('SvStringsSocket', 'Faces') + + son('SvVerticesSocket', 'Vertices') + son('SvStringsSocket', 'Edges') + son('SvStringsSocket', 'Faces') + self.width = 210 + + + self.update_sockets(context) + + def process(self): + inputs = self.inputs + Vertices = inputs["Vertices"].sv_get(default=None) + Edges = inputs["Edges"].sv_get(default=[[]]) + Faces = inputs["Faces"].sv_get(default=None) + + outputs = self.outputs + if not any( [o.is_linked for o in outputs]): + return + + res_verts = [] + res_edges = [] + res_faces = [] + for verts, edges, faces in zip_long_repeat(Vertices, Edges, Faces): + if( max(map(len, faces))!=min(map(len, faces))): + bm_I = bmesh_from_pydata(verts, edges, faces, markup_face_data=True, normal_update=True) + b_faces = [] + for face in bm_I.faces: + b_faces.append(face) + res = bmesh.ops.triangulate( bm_I, faces=b_faces, quad_method=self.quad_mode, ngon_method=self.ngon_mode ) + new_vertices_I, new_edges_I, new_faces_I = pydata_from_bmesh(bm_I) + bm_I.free() + pdmesh = PolyData.from_regular_faces( new_vertices_I, new_faces_I) + else: + pdmesh = PolyData.from_regular_faces( verts, faces) + + clust = pyacvd.Clustering(pdmesh) + if(self.cluster_subdivide>0): + clust.subdivide(self.cluster_subdivide) + clust.cluster(self.cluster_counts, maxiter=self.max_iter) + remesh = clust.create_mesh() + # remesh is triangulated here + edges0 = remesh.regular_faces[:,[0,1]] # edges AB + edges1 = remesh.regular_faces[:,[1,2]] # edges BC + edges2 = remesh.regular_faces[:,[2,0]] # edges CA + # stack + remesh_edges = np.vstack((edges0, edges1, edges2)) + + if self.output_as_numpy: + res_verts.append(remesh.points) + res_edges.append(remesh_edges) + res_faces.append(remesh.regular_faces) + else: + res_verts.append(remesh.points.tolist()) + res_edges.append(remesh_edges.tolist()) + res_faces.append(remesh.regular_faces.tolist()) + + outputs['Vertices'].sv_set(res_verts) + outputs['Edges'].sv_set(res_edges) + outputs['Faces'].sv_set(res_faces) + +def register(): + bpy.utils.register_class(SvMeshClusteringNode) + + +def unregister(): + bpy.utils.unregister_class(SvMeshClusteringNode) diff --git a/settings.py b/settings.py index d40ad08bc0..deb4ef94b5 100644 --- a/settings.py +++ b/settings.py @@ -621,6 +621,7 @@ def draw_freecad_ops(): draw_message(box, "pyOpenSubdiv") draw_message(box, "numexpr") draw_message(box, "ezdxf") + draw_message(box, "pyacvd") draw_freecad_ops()