diff --git a/docs/assets/nodes/spatial/populate_mesh_edges_1.png b/docs/assets/nodes/spatial/populate_mesh_edges_1.png new file mode 100644 index 0000000000..d2e04aa6cf Binary files /dev/null and b/docs/assets/nodes/spatial/populate_mesh_edges_1.png differ diff --git a/docs/assets/nodes/spatial/populate_mesh_field_1.png b/docs/assets/nodes/spatial/populate_mesh_field_1.png new file mode 100644 index 0000000000..0286298f60 Binary files /dev/null and b/docs/assets/nodes/spatial/populate_mesh_field_1.png differ diff --git a/docs/assets/nodes/spatial/populate_mesh_surface_1.png b/docs/assets/nodes/spatial/populate_mesh_surface_1.png new file mode 100644 index 0000000000..b7ae3b96dd Binary files /dev/null and b/docs/assets/nodes/spatial/populate_mesh_surface_1.png differ diff --git a/docs/assets/nodes/spatial/populate_mesh_surface_2.png b/docs/assets/nodes/spatial/populate_mesh_surface_2.png new file mode 100644 index 0000000000..29cf1be702 Binary files /dev/null and b/docs/assets/nodes/spatial/populate_mesh_surface_2.png differ diff --git a/docs/assets/nodes/spatial/populate_mesh_volume_2.png b/docs/assets/nodes/spatial/populate_mesh_volume_2.png new file mode 100644 index 0000000000..741d7050fc Binary files /dev/null and b/docs/assets/nodes/spatial/populate_mesh_volume_2.png differ diff --git a/docs/assets/nodes/spatial/populate_mesh_voronoi_1.png b/docs/assets/nodes/spatial/populate_mesh_voronoi_1.png new file mode 100644 index 0000000000..1c85caddca Binary files /dev/null and b/docs/assets/nodes/spatial/populate_mesh_voronoi_1.png differ diff --git a/docs/nodes/spatial/populate_mesh_mk2.rst b/docs/nodes/spatial/populate_mesh_mk2.rst new file mode 100644 index 0000000000..1f0f58744c --- /dev/null +++ b/docs/nodes/spatial/populate_mesh_mk2.rst @@ -0,0 +1,142 @@ +Populate Mesh +============= + +Functionality +------------- + +The node distributes points on given mesh. + +.. image:: https://user-images.githubusercontent.com/14288520/201531076-d2b9e049-b229-4065-acbe-a9c735364e3c.png + :target: https://user-images.githubusercontent.com/14288520/201531076-d2b9e049-b229-4065-acbe-a9c735364e3c.png + +Volume: + +.. image:: https://user-images.githubusercontent.com/14288520/201530858-6d55519a-b06d-4a90-a15d-a564d7c9e5e6.gif + :target: https://user-images.githubusercontent.com/14288520/201530858-6d55519a-b06d-4a90-a15d-a564d7c9e5e6.gif + +Category +-------- + +Spatial -> Populate Mesh + +Inputs +------ + +* **Vertices**. Vertices of given mesh(es). This input is mandatory. +* **Edges**. Edges of given mesh(es). +* **Faces**. Faces of given mesh(es). This input is mandatory. +* **Weights**. This input is optional. Available only in **Surface** and + **Edges** modes. This input is supposed to contain one value per face or + edge. The less value the less number of vertices generates on current face. + Values lessen then 0 will be changed to 0. This input can be used as mask. + Just assign 0 values to faces on which there is no need in generation points. +* **Field**. The scalar field defining the distribution of generated points. If + this input is not connected, the node will generate evenly distributed + points. This input is mandatory, if **Proportional to Field** parameter is checked. +* **Count**. The number of points to be generated. The default value is 50. +* **MinDistance**. This input is available only when **Distance** parameter is + set to **Min. Distance**. Minimum allowable distance between generated + points. If set to zero, there will be no restriction on distance between + points. Default value is 0. +* **RadiusField**. This input is available and mandatory only when **Distance** + parameter is set to **Radius Field**. The scalar field, which defines radius + of free sphere around any generated point. +* **Threshold**. Threshold value: the node will not generate points in areas + where the value of scalar field is less than this value. The default value is + 0.5. +* **Field Minimum**. Minimum value of scalar field reached within the mesh. + This input is used to define the probability of vertices generation at + certain points. This input is only available when the **Proportional to + Field** parameter is checked. The default value is 0.0. +* **Field Maximum**. Maximum value of scalar field reached within mesh. + This input is used to define the probability of vertices generation at + certain points. This input is only available when the **Proportional to Field** + parameter is checked. The default value is 1.0. +* **Seed**. Random seed. The default value is 0. + +Parameters +---------- + +This node has the following parameters: + +* **Mode**. This defines where the points will be generated. The available options are: + + * **Volume**. The points will be generated inside the mesh. + * **Surface**. The points will be generated on the surface of the mesh. + * **Edges**. The points will be generated on the edges of the mesh. + + The default value is **Volume**. +* **Distance**. This defines how minimum distance between generated points is + defined. The available options are: + + * **Min. Distance**. The user provides minimum distance between any two + points in the **MinDistance** input. + * **RadiusField**. The user defines a radius of a sphere that should be + empty around each generated point, by providing a scalar field in the + **RadiusField** input. The node makes sure that these spheres will not + intersect. + + The default value is **Min. Distance**. +* **Proportional to Field**. If checked, then the points density will be distributed + proportionally to the values of scalar field. Otherwise, the points will be + uniformly distributed in the area where the value of scalar field exceeds + threshold. Unchecked by default. +* **Proportional to Face Area / Edge Length**. This parameter is available only + in **Surface** and **Edges** modes. If checked then the points density will + be distributed proportionally to areas of mesh faces (in **Surface** mode) or + length of mesh edges (in **Edges** mode). Checked by default. +* **Random Radius**. This parameter is available only when **Distance** + parameter is set to **RadiusField**. If checked, then radiuses of empty + spheres will be generated randomly, by using uniform distribution between 0 + (zero) and the value defined by the scalar field provided in the + **RadiusField** input. Unchecked by default. + +Outputs +------- + +This node has the following output: + +* **Vertices**. Generated vertices. +* **Indices**. This output is available only in **Surface** and **Edges** + modes. For each generated point, it contains the index of mesh face or edge + the point was generated on. +* **Radiuses**. This output is available only when **Distance** parameter is + set to **RadiusField**. For each generated point, this output contains the + radius of free space around the point. + +Examples of Usage +----------------- + +Points on the surface of cricket model: + +.. image:: https://user-images.githubusercontent.com/14288520/201532606-a738a7b3-c303-44d6-ab81-3f49b0c11468.gif + :target: https://user-images.githubusercontent.com/14288520/201532606-a738a7b3-c303-44d6-ab81-3f49b0c11468.gif + +.. image:: ../../../docs/assets/nodes/spatial/populate_mesh_surface_1.png + :target: ../../../docs/assets/nodes/spatial/populate_mesh_surface_1.png + +Points within mesh volume, with distribution controlled by scalar field: + +.. image:: ../../../docs/assets/nodes/spatial/populate_mesh_volume_2.png + :target: ../../../docs/assets/nodes/spatial/populate_mesh_volume_2.png + +Points on mesh surface, with distribution controlled by scalar field: + +.. image:: ../../../docs/assets/nodes/spatial/populate_mesh_surface_2.png + :target: ../../../docs/assets/nodes/spatial/populate_mesh_surface_2.png + +Points on mesh edges, with distribution controlled by scalar field: + +.. image:: ../../../docs/assets/nodes/spatial/populate_mesh_edges_1.png + :target: ../../../docs/assets/nodes/spatial/populate_mesh_edges_1.png + +Points on mesh surface, with distribution controlled by scalar field: + +.. image:: ../../../docs/assets/nodes/spatial/populate_mesh_field_1.png + :target: ../../../docs/assets/nodes/spatial/populate_mesh_field_1.png + +Voronoi built from mesh, with sites distribution controlled by scalar field: + +.. image:: ../../../docs/assets/nodes/spatial/populate_mesh_voronoi_1.png + :target: ../../../docs/assets/nodes/spatial/populate_mesh_voronoi_1.png + diff --git a/docs/nodes/spatial/random_points_on_mesh.rst b/docs/nodes/spatial/random_points_on_mesh.rst index 580fbe7ab3..92d595b8a4 100644 --- a/docs/nodes/spatial/random_points_on_mesh.rst +++ b/docs/nodes/spatial/random_points_on_mesh.rst @@ -148,4 +148,4 @@ Examples * Viz-> :doc:`Viewer Draw ` .. image:: https://user-images.githubusercontent.com/14288520/201540714-bc24a930-2fb8-439a-8883-44525bf9bb74.gif - :target: https://user-images.githubusercontent.com/14288520/201540714-bc24a930-2fb8-439a-8883-44525bf9bb74.gif \ No newline at end of file + :target: https://user-images.githubusercontent.com/14288520/201540714-bc24a930-2fb8-439a-8883-44525bf9bb74.gif diff --git a/docs/tutorials/Celling_00/celling_02.rst b/docs/tutorials/Celling_00/celling_02.rst index 62b7e524c7..c42f2e8ad2 100644 --- a/docs/tutorials/Celling_00/celling_02.rst +++ b/docs/tutorials/Celling_00/celling_02.rst @@ -77,7 +77,6 @@ Demonstrating from one side: 1. Every plate merged by distance to be sure it consistent and sorted to manipulate after bisect disorder; 2. Mask formula. For every edge find out thouse of edges, are vertically oriented. That edges are bend edges, that needed to be joined when sewing left and right; 3. Loop for every plate. Input to that loop original "triangle" and inseted (gap between plates); - 4. 7. **Tipisation of plates**: diff --git a/docs/tutorials/celling.rst b/docs/tutorials/celling.rst index 5a7b5e8c43..63fbcedbc8 100644 --- a/docs/tutorials/celling.rst +++ b/docs/tutorials/celling.rst @@ -24,4 +24,3 @@ So, plus-minus optimal way of recreation invented here. Lesson 05 - Types definition with checking Lesson 06 - "Layouts" on "paper space" Lesson 07 - SVG/DXF/XMLX/IFC export - Lesson 08 - Realisation \ No newline at end of file diff --git a/index.yaml b/index.yaml index 23fbade3ab..e00976e494 100644 --- a/index.yaml +++ b/index.yaml @@ -302,7 +302,7 @@ - icon_name: POINTCLOUD_DATA - extra_menu: AdvancedObjectsPartialMenu - SvHomogenousVectorField - - SvRandomPointsOnMesh + - SvPopulateMeshNode - SvPopulateSurfaceMk2Node - SvPopulateSolidMk2Node - SvFieldRandomProbeMk3Node diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index f62cc5b015..3825c70b1d 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -498,7 +498,7 @@ - icon_name: POINTCLOUD_DATA - extra_menu: AdvancedObjectsPartialMenu - SvHomogenousVectorField - - SvRandomPointsOnMesh + - SvPopulateMeshNode - SvPopulateSurfaceMk2Node - SvPopulateSolidMk2Node - SvFieldRandomProbeMk3Node diff --git a/nodes/spatial/populate_mesh_mk2.py b/nodes/spatial/populate_mesh_mk2.py new file mode 100644 index 0000000000..058e8ef7fc --- /dev/null +++ b/nodes/spatial/populate_mesh_mk2.py @@ -0,0 +1,287 @@ +# 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 numpy as np +import bpy +from bpy.props import FloatProperty, BoolProperty, EnumProperty, IntProperty +import bmesh + +from sverchok.core.sv_custom_exceptions import SvNoDataError +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, get_data_nesting_level, ensure_nesting_level, zip_long_repeat +from sverchok.utils.bvh_tree import bvh_tree_from_polygons +from sverchok.utils.field.scalar import SvScalarField +from sverchok.utils.mesh_spatial import populate_mesh_edges, populate_mesh_volume, populate_mesh_surface +from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh + +class SvPopulateMeshNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Populate Mesh + Tooltip: Generate random points within mesh + """ + bl_idname = 'SvPopulateMeshNode' + bl_label = 'Populate Mesh' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_POPULATE_SOLID' + + def update_sockets(self, context): + self.inputs['FieldMin'].hide_safe = self.proportional_field != True + self.inputs['FieldMax'].hide_safe = self.proportional_field != True + self.inputs['RadiusField'].hide_safe = self.distance_mode != 'FIELD' + self.inputs['MinDistance'].hide_safe = self.distance_mode != 'CONST' + self.inputs['Weights'].hide_safe = self.gen_mode == 'VOLUME' + self.outputs['Radiuses'].hide_safe = self.distance_mode != 'FIELD' + self.outputs['Indices'].hide_safe = self.gen_mode not in {'SURFACE', 'EDGES'} + updateNode(self, context) + + modes = [ + ('VOLUME', "Volume", "Generate points inside mesh", 0), + ('SURFACE', "Surface", "Generate points on the surface of the mesj", 1), + ('EDGES', "Edges", "Generate points on edges", 2) + ] + + gen_mode : EnumProperty( + name = "Generation mode", + items = modes, + default = 'VOLUME', + update = update_sockets) + + threshold : FloatProperty( + name = "Threshold", + default = 0.5, + description="The node will not generate points in areas where the value of scalar field is less than this value", + update = updateNode) + + field_min : FloatProperty( + name = "Field Minimum", + default = 0.0, + description="Minimum value of scalar field reached within the area defined by Bounds input. This input is used to define the probability of vertices generation at certain points", + update = updateNode) + + field_max : FloatProperty( + name = "Field Maximum", + default = 1.0, + description="Maximum value of scalar field reached within the area defined by Bounds input. This input is used to define the probability of vertices generation at certain points", + update = updateNode) + + seed: IntProperty(default=0, name='Seed', description="Random seed", update=updateNode) + + count : IntProperty( + name = "Count", + default = 50, + min = 1, + description="The number of points to be generated", + update = updateNode) + + proportional_field : BoolProperty( + name = "Proportional to Field", + default = False, + description="If checked, then the points density will be distributed proportionally to the values of scalar field. Otherwise, the points will be uniformly distributed in the area where the value of scalar field exceeds threshold", + update = update_sockets) + + proportional_faces : BoolProperty( + name = "Proportional to Face Area", + default = True, + description="If checked, then number of points at each face is proportional to the area of the face", + update = update_sockets) + + min_r : FloatProperty( + name = "Min.Distance", + description = "Minimum distance between generated points; set to 0 to disable the check", + default = 0.0, + min = 0.0, + update = updateNode) + + distance_modes = [ + ('CONST', "Min. Distance", "Specify minimum distance between points", 0), + ('FIELD', "Radius Field", "Specify radius of empty sphere around each point by scalar field", 1) + ] + + distance_mode : EnumProperty( + name = "Distance", + description = "How minimum distance between points is restricted", + items = distance_modes, + default = 'CONST', + update = update_sockets) + + random_radius : BoolProperty( + name = "Random radius", + description = "Make sphere radiuses random, restricted by scalar field values", + default = False, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.label(text='Mode:') + layout.prop(self, "gen_mode", text='') + layout.label(text='Distance:') + layout.prop(self, 'distance_mode', text='') + layout.prop(self, "proportional_field") + if self.gen_mode == 'SURFACE': + layout.prop(self, "proportional_faces", text="Proportional to Face Area") + elif self.gen_mode == 'EDGES': + layout.prop(self, "proportional_faces", text="Proportional to Edge Length") + if self.distance_mode == 'FIELD': + layout.prop(self, 'random_radius') + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices") + self.inputs.new('SvStringsSocket', "Edges") + self.inputs.new('SvStringsSocket', "Faces") + self.inputs.new('SvStringsSocket', "Weights") + self.inputs.new('SvScalarFieldSocket', "Field").enable_input_link_menu = False + self.inputs.new('SvStringsSocket', "Count").prop_name = 'count' + self.inputs.new('SvStringsSocket', "MinDistance").prop_name = 'min_r' + self.inputs.new('SvScalarFieldSocket', "RadiusField") + self.inputs.new('SvStringsSocket', "Threshold").prop_name = 'threshold' + self.inputs.new('SvStringsSocket', "FieldMin").prop_name = 'field_min' + self.inputs.new('SvStringsSocket', "FieldMax").prop_name = 'field_max' + self.inputs.new('SvStringsSocket', 'Seed').prop_name = 'seed' + self.outputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvStringsSocket', "Indices") + self.outputs.new('SvStringsSocket', "Radiuses") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + if self.proportional_field and not self.inputs['Field'].is_linked: + raise SvNoDataError(socket=self.inputs['Field'], node=self) + + verts_s = self.inputs['Vertices'].sv_get() + if self.inputs['Edges'].is_linked: + edges_s = self.inputs['Edges'].sv_get() + edges_s = ensure_nesting_level(edges_s, 4) + else: + edges_s = [[[None]]] + faces_s = self.inputs['Faces'].sv_get() + faces_s = ensure_nesting_level(faces_s, 4) + if self.gen_mode in {'SURFACE', 'EDGES'} and self.inputs['Weights'].is_linked: + weights_s = self.inputs['Weights'].sv_get() + weights_s = ensure_nesting_level(weights_s, 3) + else: + weights_s = [[None]] + fields_s = self.inputs['Field'].sv_get(default=[[None]]) + count_s = self.inputs['Count'].sv_get() + min_r_s = self.inputs['MinDistance'].sv_get() + threshold_s = self.inputs['Threshold'].sv_get() + field_min_s = self.inputs['FieldMin'].sv_get() + field_max_s = self.inputs['FieldMax'].sv_get() + seed_s = self.inputs['Seed'].sv_get() + if self.distance_mode == 'FIELD': + radius_s = self.inputs['RadiusField'].sv_get() + else: + radius_s = [[None]] + + input_level = get_data_nesting_level(verts_s) + verts_s = ensure_nesting_level(verts_s, 4) + nested_mesh = input_level > 3 + if self.inputs['Field'].is_linked: + input_level = get_data_nesting_level(fields_s, data_types=(SvScalarField,)) + nested_field = input_level > 1 + fields_s = ensure_nesting_level(fields_s, 2, data_types=(SvScalarField,)) + else: + nested_field = False + if self.distance_mode == 'FIELD': + input_level = get_data_nesting_level(radius_s, data_types=(SvScalarField,)) + nested_radius = input_level > 1 + radius_s = ensure_nesting_level(radius_s, 2, data_types=(SvScalarField,)) + else: + nested_radius = False + count_s = ensure_nesting_level(count_s, 2) + min_r_s = ensure_nesting_level(min_r_s, 2) + threshold_s = ensure_nesting_level(threshold_s, 2) + field_min_s = ensure_nesting_level(field_min_s, 2) + field_max_s = ensure_nesting_level(field_max_s, 2) + seed_s = ensure_nesting_level(seed_s, 2) + + nested_output = nested_mesh or nested_field or nested_radius + + verts_out = [] + radius_out = [] + indices_out = [] + inputs = zip_long_repeat(verts_s, edges_s, faces_s, weights_s, + fields_s, count_s, + min_r_s, radius_s, threshold_s, + field_min_s, field_max_s, seed_s) + + for objects in inputs: + new_verts = [] + new_radius = [] + new_indices = [] + for verts, edges, faces, weights, field, count, min_r, radius_field, threshold, field_min, field_max, seed in zip_long_repeat(*objects): + bm = bmesh_from_pydata(verts, edges, faces, markup_face_data=True) + if self.gen_mode in {'SURFACE', 'VOLUME'}: + bmesh.ops.triangulate( + bm, faces=bm.faces, quad_method='FIXED', ngon_method='EAR_CLIP' + ) + idxs_layer = bm.faces.layers.int.get("initial_index") + if weights is not None: + weights = [weights[f[idxs_layer]] for f in bm.faces] + verts, edges, faces = pydata_from_bmesh(bm) + if seed == 0: + seed = 12345 + np.random.seed(seed) + if self.distance_mode == 'FIELD': + min_r = 0 + if self.gen_mode == 'VOLUME': + bvh = bvh_tree_from_polygons(verts, faces, + all_triangles=True, + epsilon=0.0, safe_check=True) + verts, radiuses = populate_mesh_volume(verts, bvh, field, count, + min_r, radius_field, threshold, + field_min, field_max, + proportional_field = self.proportional_field, + random_radius = self.random_radius, + seed=seed) + verts = np.array(verts).tolist() + indices = [None] + elif self.gen_mode == 'SURFACE': + indices, verts, radiuses = populate_mesh_surface(bm, weights, field, + count, min_r, radius_field, + threshold, + field_min, field_max, + proportional_field = self.proportional_field, + proportional_faces = self.proportional_faces, + random_radius = self.random_radius, + seed=seed) + else: # EDGES + indices, verts, radiuses = populate_mesh_edges(verts, edges, weights, + field, count, threshold, + field_min, field_max, + min_r, radius_field, + random_radius = self.random_radius, + proportional_field = self.proportional_field, + proportional_edges = self.proportional_faces, + seed=seed) + bm.free() + + new_verts.append(verts) + new_radius.append(radiuses) + new_indices.append(indices) + + if nested_output: + verts_out.append(new_verts) + radius_out.append(new_radius) + indices_out.append(new_indices) + else: + verts_out.extend(new_verts) + radius_out.extend(new_radius) + indices_out.extend(new_indices) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Radiuses'].sv_set(radius_out) + self.outputs['Indices'].sv_set(indices_out) + + +def register(): + bpy.utils.register_class(SvPopulateMeshNode) + + +def unregister(): + bpy.utils.unregister_class(SvPopulateMeshNode) + diff --git a/nodes/spatial/random_points_on_mesh.py b/old_nodes/random_points_on_mesh.py similarity index 100% rename from nodes/spatial/random_points_on_mesh.py rename to old_nodes/random_points_on_mesh.py diff --git a/utils/field/probe.py b/utils/field/probe.py index 4d7ca921ea..fc2c2d0149 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -5,13 +5,12 @@ # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE -import random import numpy as np from sverchok.utils.sv_logging import sv_logger from sverchok.utils.kdtree import SvKdTree -BATCH_SIZE = 50 +BATCH_SIZE = 100 MAX_ITERATIONS = 1000 def _check_min_distance(v_new, vs_old, min_r): @@ -69,7 +68,7 @@ def field_random_probe(field, bbox, count, if seed == 0: seed = 12345 if seed is not None: - random.seed(seed) + np.random.seed(seed) b1, b2 = bbox x_min, y_min, z_min = b1 @@ -84,52 +83,37 @@ def field_random_probe(field, bbox, count, if iterations > MAX_ITERATIONS: sv_logger.error("Maximum number of iterations (%s) reached, stop.", MAX_ITERATIONS) break - batch_xs = [] - batch_ys = [] - batch_zs = [] - batch = [] left = count - done max_size = min(BATCH_SIZE, left) - for i in range(max_size): - x = random.uniform(x_min, x_max) - y = random.uniform(y_min, y_max) - z = random.uniform(z_min, z_max) - batch_xs.append(x) - batch_ys.append(y) - batch_zs.append(z) - batch.append((x, y, z)) - batch_xs = np.array(batch_xs)#[np.newaxis][np.newaxis] - batch_ys = np.array(batch_ys)#[np.newaxis][np.newaxis] - batch_zs = np.array(batch_zs)#[np.newaxis][np.newaxis] - batch = np.array(batch) + batch = np.random.uniform((x_min,y_min,z_min), (x_max,y_max,z_max), size=(max_size,3)) if field is None: - candidates = batch.tolist() + candidates = batch else: - values = field.evaluate_grid(batch_xs, batch_ys, batch_zs) + values = field.evaluate_grid(batch[:,0], batch[:,1], batch[:,2]) good_idxs = values >= threshold if not proportional: - candidates = batch[good_idxs].tolist() + candidates = batch[good_idxs] else: candidates = [] - for vert, value in zip(batch[good_idxs].tolist(), values[good_idxs].tolist()): - probe = random.uniform(field_min, field_max) - if probe <= value: - candidates.append(vert) + probes = np.random.uniform(field_min, field_max, size=len(batch)) + probe_idxs = probes <= values + good_idxs = np.logical_and(good_idxs, probe_idxs) + candidates = batch[good_idxs] good_radiuses = [] if min_r == 0 and min_r_field is None: good_verts = candidates good_radiuses = [0 for i in range(len(good_verts))] elif min_r_field is not None: - xs = np.array([p[0] for p in candidates]) - ys = np.array([p[1] for p in candidates]) - zs = np.array([p[2] for p in candidates]) - min_rs = min_r_field.evaluate_grid(xs, ys, zs).tolist() + min_rs = min_r_field.evaluate_grid(candidates[:,0], candidates[:,1], candidates[:,2]) + if random_radius: + min_rs = np.random.uniform( + np.zeros((len(candidates),)), + min_rs + ) good_verts = [] for candidate, min_r in zip(candidates, min_rs): - if random_radius: - min_r = random.uniform(0, min_r) if _check_min_radius(candidate, generated_verts + good_verts, generated_radiuses + good_radiuses, min_r): good_verts.append(candidate) good_radiuses.append(min_r) diff --git a/utils/mesh_spatial.py b/utils/mesh_spatial.py index 154f7c344a..c7312938a7 100644 --- a/utils/mesh_spatial.py +++ b/utils/mesh_spatial.py @@ -16,6 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### +import numpy as np from mathutils import Vector from mathutils.geometry import intersect_point_line try: @@ -25,9 +26,15 @@ from mathutils.bvhtree import BVHTree import bmesh -from sverchok.data_structure import rotate_list -from sverchok.utils.geom import linear_approximation +from sverchok.data_structure import rotate_list, numpy_full_list, repeat_last_for_length +from sverchok.utils.geom import linear_approximation,calc_bounds +from sverchok.utils.sv_mesh_utils import point_inside_mesh from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh +from sverchok.utils.sv_logging import sv_logger +from sverchok.utils.surface.populate import _check_min_radius, _check_min_distance +from sverchok.utils.field.probe import field_random_probe +from sverchok.utils.surface.primitives import SvPlane +from sverchok.utils.surface.populate import populate_surface def is_on_edge(pt, v1, v2, epsilon=1e-6): nearest, percentage = intersect_point_line(pt, v1, v2) @@ -112,3 +119,313 @@ def find_nearest_idxs(verts, faces, add_verts): idxs.append(idx) return idxs +def _verts_edges(verts, edges): + if isinstance(verts, np.ndarray): + np_verts = verts + else: + np_verts = np.array(verts) + if isinstance(edges, np.ndarray): + np_edges = edges + else: + np_edges = np.array(edges) + return np_verts[np_edges] + +BATCH_SIZE = 200 +MAX_ITERATIONS = 1000 + +def populate_mesh_volume(verts, bvh, field, count, + min_r, radius_field, threshold, + field_min, field_max, + proportional_field = False, + random_radius=False, + seed=0): + def check(vert): + result = point_inside_mesh(bvh, vert) + #print(f"{vert} => {result}") + return result + + x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(verts) + bbox = ((x_min, y_min, z_min), (x_max, y_max, z_max)) + + return field_random_probe(field, bbox, count, threshold, + proportional_field, field_min, field_max, + min_r = min_r, min_r_field = radius_field, + random_radius = random_radius, + seed = seed, predicate=check) + +def populate_mesh_surface(bm, weights, field, + total_count, min_r, min_r_field, + threshold, field_min, field_max, + proportional_field=False, + proportional_faces=True, + random_radius=False, + seed=0, predicate=None): + + def get_weights(): + nonlocal weights + if proportional_faces: + areas = [face.calc_area() for face in bm.faces] + else: + areas = [1.0 for face in bm.faces] + if weights is None: + weights = areas + else: + weights = repeat_last_for_length(weights, len(bm.faces)) + weights = [w*a for w, a in zip(weights, areas)] + + return weights + + all_idxs = np.arange(len(bm.faces)) + start_verts = np.array([f.verts[0].co for f in bm.faces]) + edges_1 = np.array([f.verts[1].co - f.verts[0].co for f in bm.faces]) + edges_2 = np.array([f.verts[2].co - f.verts[0].co for f in bm.faces]) + weights = get_weights() + + def generate_batch(batch_size): + ps = np.array(weights) / np.sum(weights) + chosen_faces = np.random.choice(all_idxs, + batch_size, + replace=True, + p = ps) + faces_with_points, points_per_face = np.unique(chosen_faces, return_counts=True) + uvs = np.random.uniform(0.0, 1.0, size=(batch_size,2)) + us = uvs[:,0] + vs = uvs[:,1] + edges_u = np.repeat(edges_1[faces_with_points], points_per_face, axis=0) + edges_v = np.repeat(edges_2[faces_with_points], points_per_face, axis=0) + start = np.repeat(start_verts[faces_with_points], points_per_face, axis=0) + chosen_indices = np.repeat(all_idxs[faces_with_points], points_per_face, axis=0) + random_points = start + edges_u * us[np.newaxis].T + edges_v * vs[np.newaxis].T + good_uv_idxs = (us + vs) <= 1 + return random_points[good_uv_idxs], chosen_indices[good_uv_idxs] + + if seed == 0: + seed = 12345 + if seed is not None: + np.random.seed(seed) + + done = 0 + iterations = 0 + generated_pts = [] + generated_idxs = [] + generated_radiuses = [] + + if field is None and min_r == 0 and min_r_field is None and predicate is None: + batch_size = total_count + else: + batch_size = BATCH_SIZE + + while done < total_count: + iterations += 1 + if iterations > MAX_ITERATIONS: + sv_logger.error("Maximum number of iterations (%s) reached, stop.", MAX_ITERATIONS) + break + left = total_count - done + size = min(batch_size, left) + batch_pts, batch_idxs = generate_batch(size) + size = len(batch_pts) + + if field is not None: + values = field.evaluate_grid(batch_pts[:,0], batch_pts[:,1], batch_pts[:,2]) + threshold_idxs = values >= threshold + if not proportional_field: + good_idxs = threshold_idxs + else: + probes = np.random.uniform(field_min, field_max, size=size) + probe_idxs = probes <= values + good_idxs = np.logical_and(threshold_idxs, probe_idxs) + candidates = batch_pts[good_idxs] + candidate_idxs = batch_idxs[good_idxs] + else: + candidates = batch_pts + candidate_idxs = batch_idxs + + good_radiuses = [] + if len(candidates) > 0: + if min_r == 0 and min_r_field is None: + good_pts = candidates + good_idxs = candidate_idxs + good_radiuses = np.zeros((len(good_pts),)) + elif min_r_field is not None: + min_rs = min_r_field.evaluate_grid(candidates[:,0], candidates[:,1], candidates[:,2]) + if random_radius: + min_rs = np.random.uniform( + np.zeros((len(candidates),)), + min_rs + ) + good_pts = [] + good_idxs = [] + for candidate_idx, candidate, min_r in zip(candidate_idxs, candidates, min_rs): + radius_ok = _check_min_radius(candidate, + generated_pts + good_pts, + generated_radiuses + good_radiuses, + min_r) + if radius_ok: + good_pts.append(candidate) + good_idxs.append(candidate_idx) + good_radiuses.append(min_r) + else: # min_r != 0: + good_pts = [] + good_idxs = [] + for candidate_idx, candidate in zip(candidate_idxs, candidates): + distance_ok = _check_min_distance(candidate, + generated_pts + good_pts, + min_r) + if distance_ok: + good_pts.append(candidate) + good_idxs.append(candidate_idx) + good_radiuses.append(0) + if predicate is not None: + res = [(i, pt, radius) for i, pt, radius in zip(good_idxs, good_pts, good_radiuses) if predicate(pt)] + good_idxs = [r[0] for r in res] + good_pts = [r[1] for r in res] + good_radiuses = [r[2] for r in res] + + generated_pts.extend(np.array(good_pts).tolist()) + generated_idxs.extend(good_idxs) + generated_radiuses.extend(good_radiuses) + done += len(good_pts) + + return generated_idxs, generated_pts, generated_radiuses + +def populate_mesh_edges(verts, edges, weights, + field, total_count, threshold, + field_min=None, field_max=None, + min_r=0, min_r_field = None, + random_radius = False, + avoid_spheres = None, + proportional_field=False, + proportional_edges=True, + seed=0, predicate=None): + + + def get_weights(edges_dir, weights): + if proportional_edges: + lengths = np.linalg.norm(edges_dir, axis=1) + else: + lengths = np.full((len(edges_dir),), 1.0) + + if weights is not None and len(weights) > 0: + weights = numpy_full_list(weights, len(edges_dir)) * lengths + else: + weights = lengths + + return weights + + if seed == 0: + seed = 12345 + if seed is not None: + np.random.seed(seed) + + v_edges = _verts_edges(verts, edges) + edges_dir = v_edges[:, 1] - v_edges[:, 0] + weights = get_weights(edges_dir, weights) + indices = np.arange(len(edges)) + + if avoid_spheres is not None: + old_points = [s[0] for s in avoid_spheres] + old_radiuses = [s[1] for s in avoid_spheres] + else: + old_points = [] + old_radiuses = [] + + def generate_batch(batch_size): + ps = np.array(weights) / np.sum(weights) + chosen_edges = np.random.choice(indices, + batch_size, + replace=True, + p=ps) + + edges_with_points, points_total_per_edge = np.unique(chosen_edges, return_counts=True) + t_s = np.random.uniform(low=0, high=1, size=batch_size) + direc = np.repeat(edges_dir[edges_with_points], points_total_per_edge, axis=0) + orig = np.repeat(v_edges[edges_with_points, 0], points_total_per_edge, axis=0) + + chosen_indices = np.repeat(indices[edges_with_points], points_total_per_edge, axis=0) + random_points = orig + direc * t_s[np.newaxis].T + + return random_points, chosen_indices + + done = 0 + iterations = 0 + generated_pts = [] + generated_idxs = [] + generated_radiuses = [] + + if field is None and avoid_spheres is None and min_r == 0 and min_r_field is None and predicate is None: + batch_size = total_count + else: + batch_size = BATCH_SIZE + while done < total_count: + if iterations > MAX_ITERATIONS: + sv_logger.error("Maximum number of iterations (%s) reached, generated only (%s) points of (%s), stop.", MAX_ITERATIONS, done, total_count) + break + iterations += 1 + left = total_count - done + size = min(batch_size, left) + batch_pts, batch_idxs = generate_batch(size) + + if field is not None: + values = field.evaluate_grid(batch_pts[:,0], batch_pts[:,1], batch_pts[:,2]) + + threshold_idxs = values >= threshold + if not proportional_field: + good_idxs = threshold_idxs + else: + probes = np.random.uniform(field_min, field_max, size=size) + probe_idxs = probes <= values + good_idxs = np.logical_and(threshold_idxs, probe_idxs) + candidates = batch_pts[good_idxs] + candidate_idxs = batch_idxs[good_idxs] + else: + candidates = batch_pts + candidate_idxs = batch_idxs + + good_radiuses = [] + if len(candidates) > 0: + if min_r == 0 and min_r_field is None: + good_pts = candidates + good_idxs = candidate_idxs + good_radiuses = np.zeros((len(good_pts),)) + elif min_r_field is not None: + min_rs = min_r_field.evaluate_grid(candidates[:,0], candidates[:,1], candidates[:,2]) + if random_radius: + min_rs = np.random.uniform( + np.zeros((len(candidates),)), + min_rs + ) + good_pts = [] + good_idxs = [] + for candidate_idx, candidate, min_r in zip(candidate_idxs, candidates, min_rs): + radius_ok = _check_min_radius(candidate, + old_points + generated_pts + good_pts, + old_radiuses + generated_radiuses + good_radiuses, + min_r) + if radius_ok: + good_pts.append(candidate) + good_idxs.append(candidate_idx) + good_radiuses.append(min_r) + else: # min_r != 0: + good_pts = [] + good_idxs = [] + for candidate_idx, candidate in zip(candidate_idxs, candidates): + distance_ok = _check_min_distance(candidate, + old_points + generated_pts + good_pts, + min_r) + if distance_ok: + good_pts.append(candidate) + good_idxs.append(candidate_idx) + good_radiuses.append(0) + if predicate is not None: + res = [(i, pt, radius) for i, pt, radius in zip(good_idxs, good_pts, good_radiuses) if predicate(pt)] + good_idxs = [r[0] for r in res] + good_pts = [r[1] for r in res] + good_radiuses = [r[2] for r in res] + + generated_pts.extend(np.array(good_pts).tolist()) + generated_idxs.extend(good_idxs) + generated_radiuses.extend(good_radiuses) + done += len(good_pts) + + return generated_idxs, generated_pts, generated_radiuses + diff --git a/utils/surface/populate.py b/utils/surface/populate.py index 8130e9fafb..17db90b77c 100644 --- a/utils/surface/populate.py +++ b/utils/surface/populate.py @@ -94,7 +94,7 @@ def populate_surface(surface, field, count, threshold, if seed == 0: seed = 12345 if seed is not None: - random.seed(seed) + np.random.seed(seed) done = 0 generated_verts = [] generated_uv = [] @@ -109,19 +109,15 @@ def populate_surface(surface, field, count, threshold, while done < count: iterations += 1 if iterations > MAX_ITERATIONS: - sv_logger.error("Maximum number of iterations (%s) reached, stop.", MAX_ITERATIONS) + sv_logger.error("Maximum number of iterations (%s) reached, generated only %s points of %s, stop.", MAX_ITERATIONS, done, count) break batch_us = [] batch_vs = [] left = count - done max_size = min(batch_size, left) - for i in range(max_size): - u = random.uniform(u_min, u_max) - v = random.uniform(v_min, v_max) - batch_us.append(u) - batch_vs.append(v) - batch_us = np.array(batch_us) - batch_vs = np.array(batch_vs) + batch_uvs = np.random.uniform((u_min,v_min), (u_max,v_max), (max_size,2)) + batch_us = batch_uvs[:,0] + batch_vs = batch_uvs[:,1] batch_ws = np.zeros_like(batch_us) batch_uvs = np.stack((batch_us, batch_vs, batch_ws)).T @@ -138,15 +134,11 @@ def populate_surface(surface, field, count, threshold, candidates = batch_verts[good_idxs] candidate_uvs = batch_uvs[good_idxs] else: - candidates = [] - candidate_uvs = [] - for uv, vert, value in zip(batch_uvs[good_idxs].tolist(), batch_verts[good_idxs].tolist(), values[good_idxs].tolist()): - probe = random.uniform(field_min, field_max) - if probe <= value: - candidates.append(vert) - candidate_uvs.append(uv) - candidates = np.array(candidates) - candidate_uvs = np.array(candidate_uvs) + probes = np.random.uniform(field_min, field_max, len(batch_verts)) + probe_idxs = probes <= values + good_idxs = np.logical_and(good_idxs, probe_idxs) + candidates = batch_verts[good_idxs] + candidate_uvs = batch_uvs[good_idxs] else: candidates = batch_verts candidate_uvs = batch_uvs