diff --git a/docs/nodes/spatial/voronoi_on_mesh_mk2.rst b/docs/nodes/spatial/voronoi_on_mesh_mk2.rst index 4f605de504..2410adfa6b 100644 --- a/docs/nodes/spatial/voronoi_on_mesh_mk2.rst +++ b/docs/nodes/spatial/voronoi_on_mesh_mk2.rst @@ -1,8 +1,8 @@ Voronoi on Mesh =============== -.. image:: https://github.com/nortikin/sverchok/assets/14288520/3782710e-2a14-4bea-a835-37ac6ff715b4 - :target: https://github.com/nortikin/sverchok/assets/14288520/3782710e-2a14-4bea-a835-37ac6ff715b4 +.. image:: https://github.com/nortikin/sverchok/assets/14288520/c907cc2d-7493-4117-a20f-b5e760a47d28 + :target: https://github.com/nortikin/sverchok/assets/14288520/c907cc2d-7493-4117-a20f-b5e760a47d28 Dependencies ------------ @@ -29,6 +29,12 @@ This node has the following inputs: * **Vertices**. Vertices of the mesh to generate Voronoi diagram on. This input is mandatory. * **Faces**. Faces of the mesh to generate Voronoi diagram on. This input is mandatory. +* **Mask**. List of True/False. What Sites will visible. + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/cc788894-acc7-4b1f-a1c0-fb5756e42eb2 + :target: https://github.com/nortikin/sverchok/assets/14288520/cc788894-acc7-4b1f-a1c0-fb5756e42eb2 + + * **Sites**. The points to generate Voronoi diagram for. Usually you want for this points to lie either inside the mesh or on it's surface, but this is not necessary. This input is mandatory. diff --git a/index.yaml b/index.yaml index e6edaf5752..c2430b59c0 100644 --- a/index.yaml +++ b/index.yaml @@ -315,7 +315,7 @@ - SvExVoronoi3DNode - SvExVoronoiSphereNode - SvVoronoiOnSurfaceNode - - SvVoronoiOnMeshNodeMK2 + - SvVoronoiOnMeshNodeMK3 - SvVoronoiOnSolidNodeMK2 - --- - SvLloyd2dNode diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index e905e2ae10..02bee714b5 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -507,7 +507,7 @@ - SvExVoronoi3DNode - SvExVoronoiSphereNode - SvVoronoiOnSurfaceNode - - SvVoronoiOnMeshNodeMK2 + - SvVoronoiOnMeshNodeMK3 - SvVoronoiOnSolidNodeMK2 - --- - SvLloyd2dNode diff --git a/menus/full_nortikin.yaml b/menus/full_nortikin.yaml index f83e340cec..ce89354f67 100644 --- a/menus/full_nortikin.yaml +++ b/menus/full_nortikin.yaml @@ -383,7 +383,7 @@ - SvExVoronoi3DNode - SvExVoronoiSphereNode - SvVoronoiOnSurfaceNode - - SvVoronoiOnMeshNodeMK2 + - SvVoronoiOnMeshNodeMK3 - SvVoronoiOnSolidNodeMK2 - --- - SvLloyd2dNode diff --git a/nodes/spatial/voronoi_on_mesh_mk2.py b/nodes/spatial/voronoi_on_mesh_mk2.py index 384d8f29db..90bfeef385 100644 --- a/nodes/spatial/voronoi_on_mesh_mk2.py +++ b/nodes/spatial/voronoi_on_mesh_mk2.py @@ -25,14 +25,15 @@ from sverchok.utils.sv_bmesh_utils import recalc_normals from sverchok.utils.sv_mesh_utils import mesh_join from sverchok.utils.voronoi3d import voronoi_on_mesh +import numpy as np -class SvVoronoiOnMeshNodeMK2(SverchCustomTreeNode, bpy.types.Node): +class SvVoronoiOnMeshNodeMK3(SverchCustomTreeNode, bpy.types.Node): """ Triggers: Voronoi Mesh Tooltip: Generate Voronoi diagram on the surface of a mesh object """ - bl_idname = 'SvVoronoiOnMeshNodeMK2' + bl_idname = 'SvVoronoiOnMeshNodeMK3' bl_label = 'Voronoi on Mesh' bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_VORONOI' @@ -80,6 +81,30 @@ def update_sockets(self, context): default = 'FLAT', update = updateNode) + def updateMaskMode(self, context): + if self.mask_mode=='MASK': + self.inputs["Mask"].label = "Mask of Sites" + elif self.mask_mode=='INDEXES': + self.inputs["Mask"].label = "Indexes of Sites" + updateNode(self, context) + + mask_modes = [ + ('MASK', "Mask of sites", "Boolean value (0/1) to mask of sites", 0), + ('INDEXES', "Index of sites", "Indexes of sites to mask", 1), + ] + mask_mode : EnumProperty( + name = "Mask mode", + items = mask_modes, + default = 'MASK', + update = updateMaskMode) + + mask_inversion : BoolProperty( + name = "Inversion", + default = False, + description="Invert mask of sites", + update = updateNode) + + accuracy : IntProperty( name = "Accuracy", description = "Accuracy for mesh bisecting procedure", @@ -92,16 +117,21 @@ def sv_init(self, context): self.inputs.new('SvStringsSocket', 'Faces') self.inputs.new('SvVerticesSocket', "Sites") # self.inputs.new('SvStringsSocket', 'Thickness').prop_name = 'thickness' + self.inputs.new('SvStringsSocket', "Mask").label = "Mask of Sites" self.inputs.new('SvStringsSocket', 'Spacing').prop_name = 'spacing' self.outputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvStringsSocket', "Edges") self.outputs.new('SvStringsSocket', "Faces") self.outputs.new('SvStringsSocket', "Sites_idx") + self.outputs.new('SvStringsSocket', "Sites_verts") self.update_sockets(context) def draw_buttons(self, context, layout): layout.label(text="Mode:") layout.prop(self, "mode", text='') + split = layout.column().split(factor=0.6) + split.column().prop(self, "mask_mode", text='') + split.column().prop(self, "mask_inversion", text='Invert') if self.mode == 'VOLUME': layout.prop(self, 'normals') layout.label(text='Output nesting:') @@ -119,6 +149,13 @@ def process(self): verts_in = self.inputs['Vertices'].sv_get(deepcopy=False) faces_in = self.inputs['Faces'].sv_get(deepcopy=False) sites_in = self.inputs['Sites'].sv_get(deepcopy=False) + + mask_in = self.inputs['Mask'] #.sv_get(deepcopy=False) + if mask_in.is_linked==False: + mask_in = [[[]]] + else: + mask_in = mask_in.sv_get(deepcopy=False) + #thickness_in = self.inputs['Thickness'].sv_get() spacing_in = self.inputs['Spacing'].sv_get(deepcopy=False) @@ -128,6 +165,7 @@ def process(self): faces_in = ensure_nesting_level(faces_in, 4) #thickness_in = ensure_nesting_level(thickness_in, 2) spacing_in = ensure_min_nesting(spacing_in, 2) + mask_in = ensure_min_nesting(mask_in, 3) nested_output = input_level > 3 @@ -137,57 +175,90 @@ def process(self): edges_out = [] faces_out = [] sites_idx_out = [] - for params in zip_long_repeat(verts_in, faces_in, sites_in, spacing_in): + sites_verts_out = [] + for params in zip_long_repeat(verts_in, faces_in, sites_in, spacing_in, mask_in): new_verts = [] new_edges = [] new_faces = [] - new_sites = [] - for verts, faces, sites, spacing in zip_long_repeat(*params): - verts, edges, faces, used_sites_idx = voronoi_on_mesh(verts, faces, sites, thickness=0, + new_sites_idx = [] + new_sites_verts = [] + for verts, faces, sites, spacing, mask in zip_long_repeat(*params): + # if mask is zero or not connected then do not mask any. Except of inversion, + if not mask: + np_mask = np.ones(len(sites), dtype=bool) + if self.mask_inversion==True: + np_mask = np.invert(np_mask) + mask = np_mask.tolist() + else: + if self.mask_mode=='MASK': + if self.mask_inversion==True: + mask = list( map( lambda v: False if v==0 else True, mask) ) + mask = mask[:len(sites)] + np_mask = np.zeros(len(sites), dtype=bool) + np_mask[0:len(mask)]=mask + np_mask = np.invert(np_mask) + mask = np_mask.tolist() + pass + elif self.mask_mode=='INDEXES': + mask_len = len(sites) + mask_range = [] + for x in mask: + if -mask_len 3 + + precision = 10 ** (-self.accuracy) + + verts_out = [] + edges_out = [] + faces_out = [] + sites_idx_out = [] + for params in zip_long_repeat(verts_in, faces_in, sites_in, spacing_in): + new_verts = [] + new_edges = [] + new_faces = [] + new_sites = [] + for verts, faces, sites, spacing in zip_long_repeat(*params): + verts, edges, faces, used_sites_idx = voronoi_on_mesh(verts, faces, sites, thickness=0, + spacing = spacing, + #clip_inner = self.clip_inner, clip_outer = self.clip_outer, + do_clip=True, clipping=None, + mode = self.mode, + normal_update = self.normals, + precision = precision) + + if self.join_mode == 'FLAT': + new_verts.extend(verts) + new_edges.extend(edges) + new_faces.extend(faces) + new_sites.extend([[idx] for idx in used_sites_idx]) + elif self.join_mode == 'SEPARATE': + new_verts.append(verts) + new_edges.append(edges) + new_faces.append(faces) + new_sites.append(used_sites_idx) + else: # JOIN + verts, edges, faces = mesh_join(verts, edges, faces) + new_verts.append(verts) + new_edges.append(edges) + new_faces.append(faces) + new_sites.append(used_sites_idx) + + if nested_output: + verts_out.append(new_verts) + edges_out.append(new_edges) + faces_out.append(new_faces) + sites_idx_out.append(new_sites) + else: + verts_out.extend(new_verts) + edges_out.extend(new_edges) + faces_out.extend(new_faces) + sites_idx_out.extend(new_sites) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Edges'].sv_set(edges_out) + self.outputs['Faces'].sv_set(faces_out) + self.outputs['Sites_idx'].sv_set(sites_idx_out) + + +def register(): + bpy.utils.register_class(SvVoronoiOnMeshNodeMK2) + + +def unregister(): + bpy.utils.unregister_class(SvVoronoiOnMeshNodeMK2) diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index 5e1c8cf18e..8f6dbf1515 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -23,6 +23,7 @@ import bpy import bmesh +import random from mathutils import Vector from mathutils.bvhtree import BVHTree @@ -236,7 +237,7 @@ def calc_bvh_projections(bvh, sites): return np.array(projections) # see additional info https://github.com/nortikin/sverchok/pull/4948 -def voronoi_on_mesh_bmesh(verts, faces, n_orig_sites, sites, spacing=0.0, mode='VOLUME', normal_update = False, precision=1e-8): +def voronoi_on_mesh_bmesh(verts, faces, n_orig_sites, sites, spacing=0.0, mode='VOLUME', normal_update = False, precision=1e-8, mask=[]): def get_sites_delaunay_params(delaunay, n_orig_sites): result = defaultdict(list) @@ -391,18 +392,37 @@ def cut_cell(start_mesh, sites_delaunay_params, site_idx, spacing, center_of_mas edges_out = [] faces_out = [] - # https://github.com/nortikin/sverchok/pull/4952 - # http://www.qhull.org/html/qdelaun.htm - # http://www.qhull.org/html/qh-optc.htm - # https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Delaunay.html - # Convert sites to 4D - np_sites = np.array([(s[0], s[1], s[2], 0) for s in sites], dtype=np.float32) - # Add 3D tetraedre to the 4D with W=1 - np_sites = np.append(np_sites, [[0.0, 0.0, 0.0, 1], - [1.0, 0.0, 0.0, 1], - [0.0, 1.0, 0.0, 1], - [0.0, 0.0, 1.0, 1], - ], axis=0) + are_sites_plane = True # plane or line + if len(sites)>=4: + # select random sites to test are they are tethraeder or 3D? + # If this thethod get wrong answer then not optimal method will be used. + list_sites_for_test_plane = random.sample( range(0, len(sites)), 4) + v0 = Vector(sites[list_sites_for_test_plane[0]]) + res = Vector(sites[list_sites_for_test_plane[1]])-v0 + for I in range(2,4): + v_I = list_sites_for_test_plane[I] + res = np.cross( res, Vector(sites[v_I])-v0 ) + res_norm = np.linalg.norm(res,ord=1) + if res_norm>0.1: + are_sites_plane = False + else: + are_sites_plane = True + + if are_sites_plane: + # https://github.com/nortikin/sverchok/pull/4952 + # http://www.qhull.org/html/qdelaun.htm + # http://www.qhull.org/html/qh-optc.htm + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Delaunay.html + # Convert sites to 4D + np_sites = np.array([(s[0], s[1], s[2], 0) for s in sites], dtype=np.float32) + # Add 3D tetraedre to the 4D with W=1 + np_sites = np.append(np_sites, [[0.0, 0.0, 0.0, 1], + [1.0, 0.0, 0.0, 1], + [0.0, 1.0, 0.0, 1], + [0.0, 0.0, 1.0, 1], + ], axis=0) + else: + np_sites = np.array([(s[0], s[1], s[2]) for s in sites], dtype=np.float32) delaunay = Delaunay(np.array(np_sites, dtype=np.float32)) sites_delaunay_params = get_sites_delaunay_params(delaunay, n_orig_sites) @@ -417,17 +437,28 @@ def cut_cell(start_mesh, sites_delaunay_params, site_idx, spacing, center_of_mas # using for precalc unneeded bisects bbox_aligned, *_ = bounding_box_aligned(verts) + # Extend mask if it is less len of sites + if len(mask)==0: + # if len of mask is 0 then use all sites + mask = [True] * ( len(sites)-len(mask) ) + else: + # else extend mask by false and do not use sites that are not in the mask + mask = mask[:]+[False]*(len(sites)-len(mask) if len(mask)<=len(sites) else 0) + start_mesh = bmesh_from_pydata(verts, [], faces, normal_update=False) used_sites_idx = [] + used_sites_verts = [] for site_idx in range(len(sites)): - cell = cut_cell(start_mesh, sites_delaunay_params, site_idx, spacing[site_idx], center_of_mass, bbox_aligned) - if cell is not None: - new_verts, new_edges, new_faces = cell - if new_verts: - verts_out.append(new_verts) - edges_out.append(new_edges) - faces_out.append(new_faces) - used_sites_idx.append( site_idx ) + if(mask[site_idx]): + cell = cut_cell(start_mesh, sites_delaunay_params, site_idx, spacing[site_idx], center_of_mass, bbox_aligned) + if cell is not None: + new_verts, new_edges, new_faces = cell + if new_verts: + verts_out.append(new_verts) + edges_out.append(new_edges) + faces_out.append(new_faces) + used_sites_idx.append( site_idx ) + used_sites_verts.append( sites[site_idx] ) start_mesh.clear() # remember to clear empty geometry start_mesh.free() @@ -436,14 +467,18 @@ def cut_cell(start_mesh, sites_delaunay_params, site_idx, spacing, center_of_mas # bisects - count of bisects in cut_cell # unb - unpredicted erased mesh (bbox_aligned cannot make predicted results) # sites - count of sites in process + # mask - mask of sites that uset to the result. Empty list all sites uset to result. # print( f"bisects: {num_bisect: 4d}, unb={num_unpredicted_erased: 4d}, sites={len(sites)}") - return verts_out, edges_out, faces_out, used_sites_idx + return verts_out, edges_out, faces_out, used_sites_idx, used_sites_verts def voronoi_on_mesh(verts, faces, sites, thickness, spacing = 0.0, clip_inner=True, clip_outer=True, do_clip=True, clipping=1.0, mode = 'REGIONS', normal_update=False, - precision = 1e-8): + precision = 1e-8, + mask = [] + ): + bvh = BVHTree.FromPolygons(verts, faces) npoints = len(sites) @@ -471,10 +506,10 @@ def voronoi_on_mesh(verts, faces, sites, thickness, else: # VOLUME, SURFACE all_points = sites[:] - verts, edges, faces, used_sites_idx = voronoi_on_mesh_bmesh(verts, faces, len(sites), all_points, + verts, edges, faces, used_sites_idx, used_sites_verts = voronoi_on_mesh_bmesh(verts, faces, len(sites), all_points, spacing = spacing, mode = mode, normal_update = normal_update, - precision = precision) - return verts, edges, faces, used_sites_idx + precision = precision, mask=mask) + return verts, edges, faces, used_sites_idx, used_sites_verts def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True, predicate_plus=None, predicate_minus=None): k = 0.5*thickness