diff --git a/docs/nodes/analyzer/weighted_vector_sum.rst b/docs/nodes/analyzer/weighted_vector_sum.rst new file mode 100644 index 0000000000..d8fe681729 --- /dev/null +++ b/docs/nodes/analyzer/weighted_vector_sum.rst @@ -0,0 +1,79 @@ +Center of Mass (Mesh) (Alpha) +============================= + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/d4ea8073-e205-4a93-867e-55c77ac81f4d + :target: https://github.com/nortikin/sverchok/assets/14288520/d4ea8073-e205-4a93-867e-55c77ac81f4d + +Functionality +------------- + +Calculate centers of mass every input mesh (with mass verts or density of materials) and calculate total center of mass: + +By edges and density: + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/23b5b892-ba5d-491a-b900-503d09242e97 + :target: https://github.com/nortikin/sverchok/assets/14288520/23b5b892-ba5d-491a-b900-503d09242e97 + +Extrapolates lists of mass of vertices: + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/bde8dfeb-ace0-4eac-b5b8-195f1700ac99 + :target: https://github.com/nortikin/sverchok/assets/14288520/bde8dfeb-ace0-4eac-b5b8-195f1700ac99 + +Using of modifier stack: + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/027a572b-e764-4009-b08b-b1acff23bb27 + :target: https://github.com/nortikin/sverchok/assets/14288520/027a572b-e764-4009-b08b-b1acff23bb27 + +Inputs +------ + +- **Vertices** - or a nested list of vertices that represent separate objects. +- **Edges** - Edges of meshes. +- **Polygons** - Polygons of meshes. +- **Mass of vertices** - Calculate of centers of mass only with vertices (ignore edges and faces). Default value is 1 mass (ex. kg, ton etc.). +- **Density** - Calculate of centers of mass only with edges or faces and ignore vertices. Default value is 1 mass/volume. + +Parameters +---------- + +- **Skip unmanifold centers** - True: If some meshes cannot be used to calc centers of mass then skip them. False - show exception + + .. image:: https://github.com/nortikin/sverchok/assets/14288520/c23429a9-44c3-47b6-bd8e-817d1cf6e4eb + :target: https://github.com/nortikin/sverchok/assets/14288520/c23429a9-44c3-47b6-bd8e-817d1cf6e4eb + +- **Center mode** - select mode to calculate center of mass (Vertices, edges, faces or volumes) + + .. image:: https://github.com/nortikin/sverchok/assets/14288520/c5bc5545-9d2a-42f0-bc59-c48caee94165 + :target: https://github.com/nortikin/sverchok/assets/14288520/c5bc5545-9d2a-42f0-bc59-c48caee94165 + + for modes **Vertices** and **Edges** triangulations are disabled (not used): + + .. image:: https://github.com/nortikin/sverchok/assets/14288520/a9136534-a9b2-4fbe-982e-45d56a437a0b + :target: https://github.com/nortikin/sverchok/assets/14288520/a9136534-a9b2-4fbe-982e-45d56a437a0b + +- **Triangulation** - used only for **Faces** or **Volumes** mode. Calculations of center of mass do triangulations before calculations. + + .. image:: https://github.com/nortikin/sverchok/assets/14288520/d3853de6-7a49-4658-87ec-c9e3b84fc3b3 + :target: https://github.com/nortikin/sverchok/assets/14288520/d3853de6-7a49-4658-87ec-c9e3b84fc3b3 + +Output +------ + +.. image:: https://github.com/nortikin/sverchok/assets/14288520/7de47244-453c-4029-bd6d-916365fd1197 + :target: https://github.com/nortikin/sverchok/assets/14288520/7de47244-453c-4029-bd6d-916365fd1197 + +- **Vertices** - Vertices of result meshes +- **Edges** - Edges of result meshes (Triangulated after **Face** or **Volume** mode) +- **Faces** - Faces of result meshes (Triangulated after **Face** or **Volume** mode) +- **Center of mass of objects** - Center of mass of every input meshes that can be calculated +- **Total center** - If there are any center of mass then this parameter contains center of mass of all meshes (depends of input params **Mass of vertices** or **Density**) +- **Counts**, **Lengths**, **Areas**, **Volumes** - params of meshes +- **Total Counts**, **Total Lengths**, **Total Areas**, **Total Volumes** - Total params of meshes +- **Masses** - List of mass of every input meshes (**Vertices** mode: total summa of vertices mass, **Edges** mode: multiply length*density, **Faces** mode: multiply areas*density, **Volumes** mode: multiply volumes*density) +- **Mass** - Summary mass of all calculated meshes +- **Mask** - Mask of calculated objects. Example: if Mode is Volume and mashes are [cube1, plane1, vertex1, cube2] then mask is [ True, False, False, True] + +Examples +-------- + +TODO \ No newline at end of file diff --git a/docs/nodes/solid/center_of_mass.rst b/docs/nodes/solid/center_of_mass.rst index 7e9dff6c39..f149f1b533 100644 --- a/docs/nodes/solid/center_of_mass.rst +++ b/docs/nodes/solid/center_of_mass.rst @@ -1,5 +1,5 @@ -Center of Mass -============== +Center of Mass (Solid) +====================== Dependencies ------------ diff --git a/index.yaml b/index.yaml index e6edaf5752..fb74a635e8 100644 --- a/index.yaml +++ b/index.yaml @@ -361,6 +361,7 @@ - SvVolumeNodeMK2 - SvAreaNode - DistancePPNode + - SvWeightedVectorSumNode - SvDistancePointLineNode - SvDistancePointPlaneNode - SvDistancetLineLineNode diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index e905e2ae10..2f99f06873 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -187,6 +187,7 @@ - SvVolumeNodeMK2 - SvAreaNode - DistancePPNode + - SvWeightedVectorSumNode - SvDistancePointLineNode - SvDistancePointPlaneNode - SvDistancetLineLineNode diff --git a/menus/full_nortikin.yaml b/menus/full_nortikin.yaml index f83e340cec..a87f3e41db 100644 --- a/menus/full_nortikin.yaml +++ b/menus/full_nortikin.yaml @@ -413,6 +413,7 @@ - SvVolumeNodeMK2 - SvAreaNode - DistancePPNode + - SvWeightedVectorSumNode - SvDistancePointLineNode - SvDistancePointPlaneNode - SvDistancetLineLineNode diff --git a/nodes/analyzer/weighted_vector_sum.py b/nodes/analyzer/weighted_vector_sum.py new file mode 100644 index 0000000000..f66705a140 --- /dev/null +++ b/nodes/analyzer/weighted_vector_sum.py @@ -0,0 +1,328 @@ +# ##### 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 +import bpy +from bpy.props import BoolVectorProperty, EnumProperty, BoolProperty, FloatProperty +import bmesh.ops +from mathutils import Matrix +from datetime import datetime + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat, repeat_last_for_length, ensure_nesting_level +from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh +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 calc_center_mass_bmesh +from sverchok.utils.modules.matrix_utils import matrix_apply_np + +from sverchok.data_structure import dataCorrect, updateNode, zip_long_repeat, match_long_repeat + +class SvWeightedVectorSumNode(SverchCustomTreeNode, bpy.types.Node, SvRecursiveNode): + """ + Triggers: Center of mass (Mesh) + Tooltip: Calculate center of mass (barycenter) of mesh per vertices, edges, faces or volume. (Volume is only for closed mesh) + """ + bl_idname = 'SvWeightedVectorSumNode' + bl_label = 'Center of mass (Mesh) (Alpha)' + bl_icon = 'SNAP_FACE_CENTER' + sv_icon = 'SV_CENTROID' + + + def updateSizeSocket(self, context): + if self.center_mode == 'VERTICES': + self.outputs['output_sizes'].label = 'Count of Verts of Every Object' + self.outputs['output_size' ].label = 'Sum Verts of All Objects' + elif self.center_mode == 'EDGES': + self.outputs['output_sizes'].label = 'Lengths of Edges of Every Object' + self.outputs['output_size' ].label = 'Summ Length of All Objects' + elif self.center_mode == 'FACES': + self.outputs['output_sizes'].label = 'Areas of Surfaces of Every Object' + self.outputs['output_size' ].label = 'Sum Area of All Objects' + elif self.center_mode == 'VOLUMES': + self.outputs['output_sizes'].label = 'Volumes of Every Object' + self.outputs['output_size' ].label = 'Sum Volume of All Objects' + + updateNode(self, context) + + center_modes = [ + ("VERTICES", "Vertices", "Calc center of mass by vertices and ignore edges, faces and volumes", 0), + ( "EDGES", "Edges", "Calc center of mass by length of edges and ignore vertices, faces and volumes", 1), + ( "FACES", "Faces", "Calc center of mass by surfaces of objects and ignore vertices, edges and volumes", 2), + ( "VOLUMES", "Volumes", "Calc center of mass by volume of objects and ignore vertices, edges and faces", 3) + ] + + 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) + ] + + skip_unmanifold_centers: BoolProperty( + name='Skip unmanifold centers', + description='If True then skip unmanifold centers else show Exception\n\nExample:\n True: if object has only vertices and \'Center mode\'=\'Volumes\', \'Faces\' or \'Edges\' then skip this object without exception', + default=True, + update=updateNode) # type: ignore + + skip_test_volume_are_closed: BoolProperty( + name='Skip test meshes for holes', + description='If True then skip test meshes for holes. (Only for \'Volumes\' mode)', + default=True, + update=updateNode) # type: ignore + + center_mode: EnumProperty( + name='Center mode', + description="Calc center mode", + items=center_modes, + default="VOLUMES", + update=updateSizeSocket) # type: ignore + + 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 + + def update_sockets(self, context): + updateNode(self, context) + + def draw_buttons(self, context, layout): + col = layout.column() + + row = col.row() + row.prop(self, 'skip_unmanifold_centers') + + row = col.row() + row.active = False + row.prop(self, 'skip_test_volume_are_closed') + if self.center_mode in ['VOLUMES']: + row.active = True + + row = col.row() + split = row.split(factor=0.4) + split.column().label(text="Center mode:") + split.column().row(align=True).prop(self, "center_mode", text='') + + row = col.row() + row.active = False + row.label(text='Triangulation (only Faces or Volumes):') + if self.center_mode in ['FACES', 'VOLUMES']: + row.active = True + + row = col.row() + row.active = False + split = row.split(factor=0.4) + split.column().label(text="Quads mode:") + split.column().row(align=True).prop(self, "quad_mode", text='') + if self.center_mode in ['FACES', 'VOLUMES']: + row.active = True + + row = col.row() + row.active = False + split = row.split(factor=0.5) + split.column().label(text="Polygons mode:") + split.column().row(align=True).prop(self, "ngon_mode", text='') + if self.center_mode in ['FACES', 'VOLUMES']: + row.active = True + + pass + + def sv_init(self, context): + self.width=220 + self.inputs.new('SvVerticesSocket', 'input_vertices') + self.inputs.new('SvStringsSocket', 'input_edges') + self.inputs.new('SvStringsSocket', 'input_polygons') + self.inputs.new('SvStringsSocket', 'input_mass_vert') + self.inputs.new('SvStringsSocket', 'input_density') + + self.inputs["input_vertices"] .label = 'Vertices' + self.inputs["input_edges"] .label = 'Edges' + self.inputs["input_polygons"] .label = 'Polygons' + self.inputs["input_mass_vert"].label = 'Mass of vertiсes' + self.inputs["input_density"] .label = 'Density' + + + self.outputs.new('SvVerticesSocket', 'output_vertices') + self.outputs.new('SvStringsSocket', 'output_edges') + self.outputs.new('SvStringsSocket', 'output_polygons') + + self.outputs.new('SvVerticesSocket', 'output_centers_of_mass') + self.outputs.new('SvVerticesSocket', 'output_total_center') + self.outputs.new('SvStringsSocket', 'output_sizes') + self.outputs.new('SvStringsSocket', 'output_size') + self.outputs.new('SvStringsSocket', 'output_masses') + self.outputs.new('SvStringsSocket', 'output_mass') + self.outputs.new('SvStringsSocket', 'output_mask') + + self.outputs['output_vertices'] .label = 'Vertices' + self.outputs['output_edges'] .label = 'Edges' + self.outputs['output_polygons'] .label = 'Polygons' + self.outputs['output_centers_of_mass'].label = 'Center mass of Every objects' + self.outputs['output_total_center'].label = 'Center mass of All objects' + self.outputs['output_sizes'].label = '' + self.outputs['output_size'] .label = '' + self.outputs['output_masses'] .label = 'Masses of Every Object' + self.outputs['output_mass'] .label = 'Mass of All Object' + self.outputs['output_mask'] .label = 'Mask Validity Every Object' + + self.updateSizeSocket(context) + self.update_sockets(context) + + def process(self): + outputs = self.outputs + if not any( [o.is_linked for o in outputs]): + return + + if not (self.inputs['input_vertices'].is_linked): + return + if not (any(self.outputs[name].is_linked for name in [ + 'output_vertices', 'output_edges', 'output_polygons', 'output_centers_of_mass', 'output_total_center', 'output_sizes', 'output_size', 'output_masses', 'output_mass', 'output_mask',])): + return + + input_vertices_s = self.inputs['input_vertices'].sv_get(default=[[]], deepcopy=False) + input_vertices_s_3 = ensure_nesting_level(input_vertices_s, 3) + input_edges_s = self.inputs['input_edges'].sv_get(default=[[]], deepcopy=False) + input_edges_s_3 = ensure_nesting_level(input_edges_s, 3) + input_polygons_s = self.inputs['input_polygons'].sv_get(default=[[]], deepcopy=False) + input_polygons_s_3 = ensure_nesting_level(input_polygons_s, 3) + input_mass_vert_s = self.inputs['input_mass_vert'].sv_get(default=[[1]], deepcopy=False) + input_mass_vert_s_2 = ensure_nesting_level(input_mass_vert_s, 2) + input_density_s = self.inputs['input_density'].sv_get(default=[[1]], deepcopy=False) + input_density_s_2 = ensure_nesting_level(input_density_s, 2) + + result_vertices_list = [] + result_edges_list = [] + result_polygons_list = [] + + + center_mass_mesh_list_out = [] + result_masses = [] + result_mask_list = [] # what object has result + + result_center_mass_mesh_list = [] + mass_mesh_list = [] + size_mesh_list = [] + + mass_center_general = None + + # If density is one level list and objects are many then + # spead list of density for every mesh. ex.: [[mesh1],[mesh2],[mesh3]], [density:d1,d2,d3] ] => [[mesh1],[mesh2],[mesh3]], [density:[d1],[d2],[d3]] ] + if len(input_density_s_2)==1 and len(input_vertices_s_3)>1: + input_density_s_2 = [[d] for d in input_density_s_2[0]] + + # if lists of edges or faces is less vertices then append at the end of edges and faces empty list to extend it later with empty items. + # verts=[[v1,v2,v3],[v4,v5,v6]] + # edges=[[[0,1],[1,2]]]=>[[[0,1], [1,2]],[]] + # faces=[[[0,1,2]]]=>[[[0,1,2]]],[]] + # this allow skip unmanifold centers + if len(input_edges_s_3)3: + # triangulate mesh for 'FACES' mode + bm_I = bmesh_from_pydata(vertices_I, edges_I, faces_I, 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=quad_mode, ngon_method=ngon_mode + ) + new_vertices_I, new_edges_I, new_faces_I = pydata_from_bmesh(bm_I) + bm_I.free() + else: + new_vertices_I,new_edges_I,new_faces_I = vertices_I, edges_I, faces_I + + result_vertices_I = new_vertices_I + result_edges_I = new_edges_I + result_polygons_I = new_faces_I + + verts_I_np = np.array(new_vertices_I) + faces_I_np = np.array(new_faces_I) + tris_I_np = verts_I_np[faces_I_np] + areases_I = np.linalg.norm(np.cross(tris_I_np[:,1]-tris_I_np[:,0], tris_I_np[:,2]-tris_I_np[:,0]) / 2.0, axis=1) + area_I = areases_I.sum() + tris_centers_I = tris_I_np.sum(axis=1) / 3.0 + center_mass_mesh_I_np = (tris_centers_I * areases_I[np.newaxis].T).sum(axis=0) / area_I + mass_I = area_I * density_I[0] + + result_center_mass_mesh_I = center_mass_mesh_I_np.tolist() + result_mass_mesh_I = mass_I + result_size_mesh_I = area_I + + elif center_mode=='VOLUMES': + + faces_I_np = np.array(faces_I) + if faces_I_np.size==0: + # skip mesh if no faces at all: + result_mask = False + else: + do_calc = False + if max(map(len, faces_I_np))==3 and skip_test_volume_are_closed==True: + new_vertices_I, new_edges_I, new_faces_I = vertices_I, edges_I, faces_I + do_calc = True + else: + # triangulate mesh + bm_I = bmesh_from_pydata(vertices_I, edges_I, faces_I, markup_face_data=True, normal_update=True) + # test if all edges are contiguous (https://docs.blender.org/api/current/bmesh.types.html#bmesh.types.BMEdge.is_contiguous) + # then volume is closed: + for edge in bm_I.edges: + if edge.is_contiguous==False: + do_calc = False + break + else: + do_calc = True + # test if some polygons are not tris: + if max(map(len, faces_I_np))>3: + b_faces = [] + for face in bm_I.faces: + b_faces.append(face) + + res = bmesh.ops.triangulate( + bm_I, faces=b_faces, quad_method=quad_mode, ngon_method=ngon_mode + ) + new_vertices_I, new_edges_I, new_faces_I = pydata_from_bmesh(bm_I) + else: + new_vertices_I, new_edges_I, new_faces_I = vertices_I, edges_I, faces_I + bm_I.free() + + if do_calc==False: + result_mask = False + else: + result_mask = True + result_vertices_I = new_vertices_I + result_edges_I = new_edges_I + result_polygons_I = new_faces_I + + verts_I = np.array(new_vertices_I) + faces_I = np.array(new_faces_I) + tris_I = verts_I[faces_I] + # to precise calc move all mesh to median point + tris_I_median = np.median(tris_I, axis=(0,1)) + tris_I_delta = tris_I-tris_I_median + signed_volumes_I = np_dot(tris_I_delta[:,0], np.cross(tris_I_delta[:,1], tris_I_delta[:,2])) + volume_I = signed_volumes_I.sum() + tetra_centers_I = tris_I_delta.sum(axis=1) / 4.0 + center_mass_mesh_I_np = (tetra_centers_I * signed_volumes_I[np.newaxis].T).sum(axis=0) / volume_I + tris_I_median + mass_I = volume_I * density_I[0] + + result_center_mass_mesh_I = center_mass_mesh_I_np.tolist() + result_mass_mesh_I = mass_I + result_size_mesh_I = volume_I + else: + raise Exception(f'calc_center_of_mesh: param \'center_mode\'={center_mode} must be in [\'VERTICES\', \'EDGES\', \'FACES\', \'VOLUMES\']') + + return result_mask, result_vertices_I, result_edges_I, result_polygons_I, result_center_mass_mesh_I, result_mass_mesh_I, result_size_mesh_I \ No newline at end of file