From cbfc3727901065df4c4831d8846e7737320c6790 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Wed, 2 Dec 2020 19:56:23 +0500 Subject: [PATCH 01/39] "Field random Probe" mk3. --- index.md | 2 +- nodes/spatial/field_random_probe.py | 104 ++++++++++++++++--- old_nodes/field_random_probe_mk2.py | 149 ++++++++++++++++++++++++++++ utils/field/probe.py | 52 ++++++++-- 4 files changed, 282 insertions(+), 25 deletions(-) create mode 100644 old_nodes/field_random_probe_mk2.py diff --git a/index.md b/index.md index 7f6f61945..aaf8d5e65 100644 --- a/index.md +++ b/index.md @@ -315,7 +315,7 @@ SvRandomPointsOnMesh SvPopulateSurfaceNode SvPopulateSolidNode - SvFieldRandomProbeMk2Node + SvFieldRandomProbeMk3Node --- DelaunayTriangulation2DNode SvDelaunay2DCdt diff --git a/nodes/spatial/field_random_probe.py b/nodes/spatial/field_random_probe.py index aced95013..cb54de5ea 100644 --- a/nodes/spatial/field_random_probe.py +++ b/nodes/spatial/field_random_probe.py @@ -20,12 +20,12 @@ from sverchok.core.socket_data import SvNoDataError from sverchok.utils.field.scalar import SvScalarField from sverchok.utils.field.probe import field_random_probe -class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): +class SvFieldRandomProbeMk3Node(bpy.types.Node, SverchCustomTreeNode): """ Triggers: Scalar Field Random Probe Tooltip: Generate random points according to scalar field """ - bl_idname = 'SvFieldRandomProbeMk2Node' + bl_idname = 'SvFieldRandomProbeMk3Node' bl_label = 'Field Random Probe' bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_FIELD_RANDOM_PROBE' @@ -53,6 +53,26 @@ class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): min = 1, update = updateNode) + @throttle_and_update_node + def update_sockets(self, context): + self.inputs['FieldMin'].hide_safe = self.proportional != True + self.inputs['FieldMax'].hide_safe = self.proportional != True + self.inputs['RadiusField'].hide_safe = self.distance_mode != 'FIELD' + self.inputs['MinDistance'].hide_safe = self.distance_mode != 'CONST' + self.outputs['Radius'].hide_safe = self.distance_mode != 'FIELD' + + 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) + min_r : FloatProperty( name = "Min.Distance", description = "Minimum distance between generated points; set to 0 to disable the check", @@ -60,18 +80,30 @@ class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): min = 0.0, update = updateNode) - @throttle_and_update_node - def update_sockets(self, context): - self.inputs['FieldMin'].hide_safe = self.proportional != True - self.inputs['FieldMax'].hide_safe = self.proportional != True - proportional : BoolProperty( name = "Proportional", + description = "Make points density proportional to field value", default = False, update = update_sockets) + random_radius : BoolProperty( + name = "Random radius", + description = "Make sphere radiuses random, restricted by scalar field values", + default = False, + update = updateNode) + + flat_output : BoolProperty( + name = "Flat output", + description = "If checked, generate one flat list of vertices for all input fields; otherwise, generate a separate list of vertices for each field", + default = True, + update = updateNode) + def draw_buttons(self, context, layout): - layout.prop(self, "proportional", toggle=True) + layout.prop(self, 'distance_mode') + layout.prop(self, "proportional") + if self.distance_mode == 'FIELD': + layout.prop(self, 'random_radius') + layout.prop(self, "flat_output") class BoundsMenuHandler(): @classmethod @@ -90,11 +122,13 @@ class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvVerticesSocket', "Bounds").link_menu_handler = 'BoundsMenuHandler' 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', "Radius") self.update_sockets(context) def get_bounds(self, vertices): @@ -111,6 +145,10 @@ class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): raise SvNoDataError(socket=self.inputs['Field'], node=self) fields_s = self.inputs['Field'].sv_get(default=[[None]]) + if self.distance_mode == 'FIELD': + radius_s = self.inputs['RadiusField'].sv_get() + else: + radius_s = [[None]] vertices_s = self.inputs['Bounds'].sv_get() count_s = self.inputs['Count'].sv_get() min_r_s = self.inputs['MinDistance'].sv_get() @@ -121,6 +159,19 @@ class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): if self.inputs['Field'].is_linked: fields_s = ensure_nesting_level(fields_s, 2, data_types=(SvScalarField,)) + input_level = get_data_nesting_level(fields_s, data_types=(SvScalarField,)) + nested_field = input_level > 1 + else: + nested_field = False + if self.inputs['RadiusField'].is_linked: + radius_s = ensure_nesting_level(radius_s, 2, data_types=(SvScalarField,)) + input_level = get_data_nesting_level(radius_s, data_types=(SvScalarField,)) + nested_radius = input_level > 1 + else: + nested_radius = False + + verts_level = get_data_nesting_level(vertices_s) + nested_verts = verts_level > 3 vertices_s = ensure_nesting_level(vertices_s, 4) count_s = ensure_nesting_level(count_s, 2) min_r_s = ensure_nesting_level(min_r_s, 2) @@ -129,21 +180,46 @@ class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): field_max_s = ensure_nesting_level(field_max_s, 2) seed_s = ensure_nesting_level(seed_s, 2) + nested_output = nested_field or nested_radius or nested_verts + verts_out = [] - inputs = zip_long_repeat(fields_s, vertices_s, threshold_s, field_min_s, field_max_s, count_s, min_r_s, seed_s) + radius_out = [] + inputs = zip_long_repeat(fields_s, radius_s, vertices_s, threshold_s, field_min_s, field_max_s, count_s, min_r_s, seed_s) for objects in inputs: - for field, vertices, threshold, field_min, field_max, count, min_r, seed in zip_long_repeat(*objects): + new_verts = [] + new_radiuses = [] + for field, radius_field, vertices, threshold, field_min, field_max, count, min_r, seed in zip_long_repeat(*objects): bbox = self.get_bounds(vertices) - new_verts = field_random_probe(field, bbox, count, threshold, self.proportional, field_min, field_max, min_r, seed) - + if self.distance_mode == 'FIELD': + min_r = 0 + verts, radiuses = field_random_probe(field, bbox, count, + threshold, self.proportional, + field_min, field_max, + min_r = min_r, min_r_field = radius_field, + random_radius = self.random_radius, + seed = seed) + + if self.flat_output: + new_verts.extend(verts) + new_radiuses.extend(radiuses) + else: + new_verts.append(verts) + new_radiuses.append(radiuses) + + if nested_output: verts_out.append(new_verts) + radius_out.append(new_radiuses) + else: + verts_out.extend(new_verts) + radius_out.extend(new_radiuses) self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Radius'].sv_set(radius_out) def register(): - bpy.utils.register_class(SvFieldRandomProbeMk2Node) + bpy.utils.register_class(SvFieldRandomProbeMk3Node) def unregister(): - bpy.utils.unregister_class(SvFieldRandomProbeMk2Node) + bpy.utils.unregister_class(SvFieldRandomProbeMk3Node) diff --git a/old_nodes/field_random_probe_mk2.py b/old_nodes/field_random_probe_mk2.py new file mode 100644 index 000000000..aced95013 --- /dev/null +++ b/old_nodes/field_random_probe_mk2.py @@ -0,0 +1,149 @@ +# 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 random +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty +from mathutils.kdtree import KDTree + +import sverchok +from sverchok.core.sockets import setup_new_node_location +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_update_node, ensure_nesting_level, get_data_nesting_level +from sverchok.core.socket_data import SvNoDataError +from sverchok.utils.field.scalar import SvScalarField +from sverchok.utils.field.probe import field_random_probe + +class SvFieldRandomProbeMk2Node(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Scalar Field Random Probe + Tooltip: Generate random points according to scalar field + """ + bl_idname = 'SvFieldRandomProbeMk2Node' + bl_label = 'Field Random Probe' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_FIELD_RANDOM_PROBE' + + threshold : FloatProperty( + name = "Threshold", + default = 0.5, + update = updateNode) + + field_min : FloatProperty( + name = "Field Minimum", + default = 0.0, + update = updateNode) + + field_max : FloatProperty( + name = "Field Maximum", + default = 1.0, + update = updateNode) + + seed: IntProperty(default=0, name='Seed', update=updateNode) + + count : IntProperty( + name = "Count", + default = 50, + min = 1, + update = updateNode) + + 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) + + @throttle_and_update_node + def update_sockets(self, context): + self.inputs['FieldMin'].hide_safe = self.proportional != True + self.inputs['FieldMax'].hide_safe = self.proportional != True + + proportional : BoolProperty( + name = "Proportional", + default = False, + update = update_sockets) + + def draw_buttons(self, context, layout): + layout.prop(self, "proportional", toggle=True) + + class BoundsMenuHandler(): + @classmethod + def get_items(cls, socket, context): + return [("BOX", "Add Box node", "Add Box node")] + + @classmethod + def on_selected(cls, tree, node, socket, item, context): + new_node = tree.nodes.new('SvBoxNodeMk2') + new_node.label = "Bounds" + tree.links.new(new_node.outputs[0], node.inputs['Bounds']) + setup_new_node_location(new_node, node) + + def sv_init(self, context): + self.inputs.new('SvScalarFieldSocket', "Field") + self.inputs.new('SvVerticesSocket', "Bounds").link_menu_handler = 'BoundsMenuHandler' + self.inputs.new('SvStringsSocket', "Count").prop_name = 'count' + self.inputs.new('SvStringsSocket', "MinDistance").prop_name = 'min_r' + 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.update_sockets(context) + + def get_bounds(self, vertices): + vs = np.array(vertices) + min = vs.min(axis=0) + max = vs.max(axis=0) + return min.tolist(), max.tolist() + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + if self.proportional and not self.inputs['Field'].is_linked: + raise SvNoDataError(socket=self.inputs['Field'], node=self) + + fields_s = self.inputs['Field'].sv_get(default=[[None]]) + vertices_s = self.inputs['Bounds'].sv_get() + 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.inputs['Field'].is_linked: + fields_s = ensure_nesting_level(fields_s, 2, data_types=(SvScalarField,)) + vertices_s = ensure_nesting_level(vertices_s, 4) + 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) + + verts_out = [] + inputs = zip_long_repeat(fields_s, vertices_s, threshold_s, field_min_s, field_max_s, count_s, min_r_s, seed_s) + for objects in inputs: + for field, vertices, threshold, field_min, field_max, count, min_r, seed in zip_long_repeat(*objects): + + bbox = self.get_bounds(vertices) + new_verts = field_random_probe(field, bbox, count, threshold, self.proportional, field_min, field_max, min_r, seed) + + verts_out.append(new_verts) + + self.outputs['Vertices'].sv_set(verts_out) + +def register(): + bpy.utils.register_class(SvFieldRandomProbeMk2Node) + +def unregister(): + bpy.utils.unregister_class(SvFieldRandomProbeMk2Node) + diff --git a/utils/field/probe.py b/utils/field/probe.py index fcdf07947..dcf65d5a4 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -16,7 +16,7 @@ from sverchok.utils.logging import error BATCH_SIZE = 50 MAX_ITERATIONS = 1000 -def _check_all(v_new, vs_old, min_r): +def _check_min_distance(v_new, vs_old, min_r): kdt = KDTree(len(vs_old)) for i, v in enumerate(vs_old): kdt.insert(v, i) @@ -26,7 +26,21 @@ def _check_all(v_new, vs_old, min_r): return True return (dist >= min_r) -def field_random_probe(field, bbox, count, threshold=0, proportional=False, field_min=None, field_max=None, min_r=0, seed=0, predicate=None): +def _check_min_radius(point, old_points, old_radiuses, min_r): + if not old_points: + return True + old_points = np.array(old_points) + old_radiuses = np.array(old_radiuses) + point = np.array(point) + distances = np.linalg.norm(old_points - point, axis=1) + ok = (old_radiuses + min_r < distances).all() + return ok + +def field_random_probe(field, bbox, count, + threshold=0, proportional=False, field_min=None, field_max=None, + min_r=0, min_r_field=None, + random_radius = False, + seed=0, predicate=None): """ Generate random points within bounding box, with distribution controlled (optionally) by a scalar field. @@ -50,16 +64,20 @@ def field_random_probe(field, bbox, count, threshold=0, proportional=False, fiel outputs: list of vertices. """ + if min_r != 0 and min_r_field is not None: + raise Exception("min_r and min_r_field can not be specified simultaneously") if seed == 0: seed = 12345 - random.seed(seed) + if seed is not None: + random.seed(seed) b1, b2 = bbox x_min, y_min, z_min = b1 x_max, y_max, z_max = b2 done = 0 - new_verts = [] + generated_verts = [] + generated_radiuses = [] iterations = 0 while done < count: iterations += 1 @@ -99,19 +117,33 @@ def field_random_probe(field, bbox, count, threshold=0, proportional=False, fiel if probe <= value: candidates.append(vert) - if min_r == 0: + good_radiuses = [] + if min_r == 0 and min_r_field is None: good_verts = candidates - else: + 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() good_verts = [] - for candidate in candidates: - if _check_all(candidate, new_verts + good_verts, min_r): + 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) + else: # min_r != 0 + good_verts = [] + for candidate in candidates: + if _check_min_distance(candidate, generated_verts + good_verts, min_r): + good_verts.append(candidate.tolist()) if predicate is not None: good_verts = [vert for vert in good_verts if predicate(vert)] - new_verts.extend(good_verts) + generated_verts.extend(good_verts) + generated_radiuses.extend(good_radiuses) done += len(good_verts) - return new_verts + return generated_verts, generated_radiuses -- GitLab From 68073237bcbc9bb9702b93d6034f35c04e069d32 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 3 Dec 2020 00:04:05 +0500 Subject: [PATCH 02/39] Populate Surface mk2. --- index.md | 2 +- nodes/spatial/populate_surface.py | 97 ++++++++++++++++++++--- old_nodes/populate_surface.py | 127 ++++++++++++++++++++++++++++++ utils/field/probe.py | 4 +- utils/surface/populate.py | 82 ++++++++++++++----- 5 files changed, 281 insertions(+), 31 deletions(-) create mode 100644 old_nodes/populate_surface.py diff --git a/index.md b/index.md index aaf8d5e65..2a1c9fdac 100644 --- a/index.md +++ b/index.md @@ -313,7 +313,7 @@ ## Spatial SvHomogenousVectorField SvRandomPointsOnMesh - SvPopulateSurfaceNode + SvPopulateSurfaceMk2Node SvPopulateSolidNode SvFieldRandomProbeMk3Node --- diff --git a/nodes/spatial/populate_surface.py b/nodes/spatial/populate_surface.py index 05393ba9b..ace1d9fef 100644 --- a/nodes/spatial/populate_surface.py +++ b/nodes/spatial/populate_surface.py @@ -19,12 +19,12 @@ from sverchok.utils.surface import SvSurface from sverchok.utils.surface.populate import populate_surface from sverchok.utils.field.scalar import SvScalarField -class SvPopulateSurfaceNode(bpy.types.Node, SverchCustomTreeNode): +class SvPopulateSurfaceMk2Node(bpy.types.Node, SverchCustomTreeNode): """ Triggers: Populate Surface Tooltip: Generate random points on the surface """ - bl_idname = 'SvPopulateSurfaceNode' + bl_idname = 'SvPopulateSurfaceMk2Node' bl_label = 'Populate Surface' bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_POPULATE_SURFACE' @@ -56,6 +56,9 @@ class SvPopulateSurfaceNode(bpy.types.Node, SverchCustomTreeNode): def update_sockets(self, context): self.inputs['FieldMin'].hide_safe = self.proportional != True self.inputs['FieldMax'].hide_safe = self.proportional != True + self.inputs['RadiusField'].hide_safe = self.distance_mode != 'FIELD' + self.inputs['MinDistance'].hide_safe = self.distance_mode != 'CONST' + self.outputs['Radiuses'].hide_safe = self.distance_mode != 'FIELD' proportional : BoolProperty( name = "Proportional", @@ -68,20 +71,50 @@ class SvPopulateSurfaceNode(bpy.types.Node, SverchCustomTreeNode): min = 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) + + flat_output : BoolProperty( + name = "Flat output", + description = "If checked, generate one flat list of vertices for all input surfaces; otherwise, generate a separate list of vertices for each surface", + default = False, + update = updateNode) + def draw_buttons(self, context, layout): + layout.prop(self, 'distance_mode') layout.prop(self, "proportional") + if self.distance_mode == 'FIELD': + layout.prop(self, 'random_radius') + layout.prop(self, "flat_output") def sv_init(self, context): self.inputs.new('SvSurfaceSocket', "Surface") 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('SvVerticesSocket', "UVPoints") + self.outputs.new('SvStringsSocket', "Radiuses") self.update_sockets(context) def process(self): @@ -98,30 +131,74 @@ class SvPopulateSurfaceNode(bpy.types.Node, SverchCustomTreeNode): field_min_s = self.inputs['FieldMin'].sv_get() field_max_s = self.inputs['FieldMax'].sv_get() min_r_s = self.inputs['MinDistance'].sv_get() + if self.distance_mode == 'FIELD': + radius_s = self.inputs['RadiusField'].sv_get() + else: + radius_s = [[None]] seed_s = self.inputs['Seed'].sv_get() + input_level = get_data_nesting_level(surface_s, data_types=(SvSurface,)) + nested_surface = input_level > 1 surface_s = ensure_nesting_level(surface_s, 2, data_types=(SvSurface,)) has_field = self.inputs['Field'].is_linked if has_field: + 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 + + nested_output = nested_surface or nested_field or nested_radius verts_out = [] uv_out = [] - - parameters = zip_long_repeat(surface_s, fields_s, count_s, threshold_s, field_min_s, field_max_s, min_r_s, seed_s) - for surfaces, fields, counts, thresholds, field_mins, field_maxs, min_rs, seeds in parameters: - objects = zip_long_repeat(surfaces, fields, counts, thresholds, field_mins, field_maxs, min_rs, seeds) - for surface, field, count, threshold, field_min, field_max, min_r, seed in objects: - new_uv, new_verts = populate_surface(surface, field, count, threshold, self.proportional, field_min, field_max, min_r, seed) + radius_out = [] + + inputs = zip_long_repeat(surface_s, fields_s, radius_s, count_s, threshold_s, field_min_s, field_max_s, min_r_s, seed_s) + for params in inputs: + new_uv = [] + new_verts = [] + new_radius = [] + for surface, field, radius, count, threshold, field_min, field_max, min_r, seed in zip_long_repeat(*params): + if self.distance_mode == 'FIELD': + min_r = 0 + uvs, verts, radiuses = populate_surface(surface, field, count, threshold, + self.proportional, field_min, field_max, + min_r = min_r, min_r_field = radius, + random_radius = self.random_radius, + seed = seed) + if self.flat_output: + new_verts.extend(verts) + new_uv.extend(uvs) + new_radius.extend(radiuses) + else: + new_verts.append(verts) + new_uv.append(uvs) + new_radius.append(radiuses) + + if nested_output: verts_out.append(new_verts) uv_out.append(new_uv) + radius_out.append(new_radius) + else: + verts_out.extend(new_verts) + uv_out.extend(new_uv) + radius_out.extend(new_radius) self.outputs['Vertices'].sv_set(verts_out) self.outputs['UVPoints'].sv_set(uv_out) + self.outputs['Radiuses'].sv_set(radius_out) def register(): - bpy.utils.register_class(SvPopulateSurfaceNode) + bpy.utils.register_class(SvPopulateSurfaceMk2Node) def unregister(): - bpy.utils.unregister_class(SvPopulateSurfaceNode) + bpy.utils.unregister_class(SvPopulateSurfaceMk2Node) diff --git a/old_nodes/populate_surface.py b/old_nodes/populate_surface.py new file mode 100644 index 000000000..05393ba9b --- /dev/null +++ b/old_nodes/populate_surface.py @@ -0,0 +1,127 @@ +# 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 random + +import bpy +from bpy.props import EnumProperty, IntProperty, BoolProperty, FloatProperty +from mathutils.kdtree import KDTree + +from sverchok.core.socket_data import SvNoDataError +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level, throttle_and_update_node +from sverchok.utils.surface import SvSurface +from sverchok.utils.surface.populate import populate_surface +from sverchok.utils.field.scalar import SvScalarField + +class SvPopulateSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Populate Surface + Tooltip: Generate random points on the surface + """ + bl_idname = 'SvPopulateSurfaceNode' + bl_label = 'Populate Surface' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_POPULATE_SURFACE' + + threshold : FloatProperty( + name = "Threshold", + default = 0.5, + update = updateNode) + + field_min : FloatProperty( + name = "Field Minimum", + default = 0.0, + update = updateNode) + + field_max : FloatProperty( + name = "Field Maximum", + default = 1.0, + update = updateNode) + + seed: IntProperty(default=0, name='Seed', update=updateNode) + + count : IntProperty( + name = "Count", + default = 50, + min = 1, + update = updateNode) + + @throttle_and_update_node + def update_sockets(self, context): + self.inputs['FieldMin'].hide_safe = self.proportional != True + self.inputs['FieldMax'].hide_safe = self.proportional != True + + proportional : BoolProperty( + name = "Proportional", + default = False, + update = update_sockets) + + min_r : FloatProperty( + name = "Min Distance", + default = 0.5, + min = 0, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "proportional") + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + 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('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('SvVerticesSocket', "UVPoints") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + if self.proportional and not self.inputs['Field'].is_linked: + raise SvNoDataError(socket=self.inputs['Field'], node=self) + + surface_s = self.inputs['Surface'].sv_get() + fields_s = self.inputs['Field'].sv_get(default=[[None]]) + count_s = self.inputs['Count'].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() + min_r_s = self.inputs['MinDistance'].sv_get() + seed_s = self.inputs['Seed'].sv_get() + + surface_s = ensure_nesting_level(surface_s, 2, data_types=(SvSurface,)) + has_field = self.inputs['Field'].is_linked + if has_field: + fields_s = ensure_nesting_level(fields_s, 2, data_types=(SvScalarField,)) + + verts_out = [] + uv_out = [] + + parameters = zip_long_repeat(surface_s, fields_s, count_s, threshold_s, field_min_s, field_max_s, min_r_s, seed_s) + for surfaces, fields, counts, thresholds, field_mins, field_maxs, min_rs, seeds in parameters: + objects = zip_long_repeat(surfaces, fields, counts, thresholds, field_mins, field_maxs, min_rs, seeds) + for surface, field, count, threshold, field_min, field_max, min_r, seed in objects: + new_uv, new_verts = populate_surface(surface, field, count, threshold, self.proportional, field_min, field_max, min_r, seed) + verts_out.append(new_verts) + uv_out.append(new_uv) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['UVPoints'].sv_set(uv_out) + +def register(): + bpy.utils.register_class(SvPopulateSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvPopulateSurfaceNode) + diff --git a/utils/field/probe.py b/utils/field/probe.py index dcf65d5a4..59a6cad9c 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -139,7 +139,9 @@ def field_random_probe(field, bbox, count, good_verts.append(candidate.tolist()) if predicate is not None: - good_verts = [vert for vert in good_verts if predicate(vert)] + pairs = [(vert, r) for vert, r in zip(good_verts, good_radiuses) if predicate(vert)] + good_verts = [p[0] for p in pairs] + good_radiuses = [p[1] for p in pairs] generated_verts.extend(good_verts) generated_radiuses.extend(good_radiuses) diff --git a/utils/surface/populate.py b/utils/surface/populate.py index 385035d43..7bb7e38f5 100644 --- a/utils/surface/populate.py +++ b/utils/surface/populate.py @@ -19,7 +19,7 @@ def random_point(min_x, max_x, min_y, max_y): y = random.uniform(min_y, max_y) return x,y -def _check_all(v_new, vs_old, min_r): +def _check_min_distance(v_new, vs_old, min_r): kdt = KDTree(len(vs_old)) for i, v in enumerate(vs_old): kdt.insert(v, i) @@ -35,10 +35,24 @@ def _check_all(v_new, vs_old, min_r): # return False # return True +def _check_min_radius(point, old_points, old_radiuses, min_r): + if not old_points: + return True + old_points = np.array(old_points) + old_radiuses = np.array(old_radiuses) + point = np.array(point) + distances = np.linalg.norm(old_points - point, axis=1) + ok = (old_radiuses + min_r < distances).all() + return ok + BATCH_SIZE = 100 MAX_ITERATIONS = 1000 -def populate_surface(surface, field, count, threshold, proportional=False, field_min=None, field_max=None, min_r=0, seed=0, predicate=None): +def populate_surface(surface, field, count, threshold, + proportional=False, field_min=None, field_max=None, + min_r=0, min_r_field = None, + random_radius = False, + seed=0, predicate=None): """ Generate random points on the surface, with distribution controlled (optionally) by scalar field. @@ -64,15 +78,20 @@ def populate_surface(surface, field, count, threshold, proportional=False, field * Coordinates of points in surface's UV space * Coordinates of points in 3D space. """ + if min_r != 0 and min_r_field is not None: + raise Exception("min_r and min_r_field can not be specified simultaneously") + u_min, u_max = surface.get_u_min(), surface.get_u_max() v_min, v_max = surface.get_v_min(), surface.get_v_max() if seed == 0: seed = 12345 - random.seed(seed) + if seed is not None: + random.seed(seed) done = 0 - new_verts = [] - new_uv = [] + generated_verts = [] + generated_uv = [] + generated_radiuses = [] iterations = 0 while done < count: @@ -120,19 +139,44 @@ def populate_surface(surface, field, count, threshold, proportional=False, field candidates = batch_verts candidate_uvs = batch_uvs + good_radiuses = [] if len(candidates) > 0: - good_verts = [] - good_uvs = [] - for candidate_uv, candidate in zip(candidate_uvs, candidates): - if min_r == 0 or _check_all(candidate, new_verts + good_verts, min_r): - if predicate is not None: - if not predicate(candidate_uv, candidate): - continue - good_verts.append(tuple(candidate)) - good_uvs.append(tuple(candidate_uv)) - done += 1 - new_verts.extend(good_verts) - new_uv.extend(good_uvs) - - return new_uv, new_verts + if min_r == 0 and min_r_field is None: + good_verts = candidates + good_uvs = candidate_uvs + 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() + good_verts = [] + good_uvs = [] + for candidate_uv, candidate, min_r in zip(candidate_uvs, 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_uvs.append(candidate_uv) + good_radiuses.append(min_r) + else: # min_r != 0 + good_verts = [] + good_uvs = [] + for candidate_uv, candidate in zip(candidate_uvs, candidates): + distance_ok = _check_min_distance(candidate, generated_verts + good_verts, min_r) + if distance_ok: + good_verts.append(tuple(candidate)) + good_uvs.append(tuple(candidate_uv)) + + if predicate is not None: + results = [(uv, vert, radius) for uv, vert, radius in zip(good_uvs, good_verts, good_radiuses) if predicate(uv, vert)] + good_uvs = [r[0] for r in results] + good_verts = [r[1] for r in results] + good_radiuses = [r[2] for r in results] + + generated_verts.extend(good_verts) + generated_uv.extend(good_uvs) + generated_radiuses.extend(good_radiuses) + done += len(good_verts) + + return generated_uv, generated_verts, generated_radiuses -- GitLab From 54fbcfdecb49b0addec2a81c1cdd052d34e38ef4 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Thu, 3 Dec 2020 16:42:01 +0500 Subject: [PATCH 03/39] Populate Solid Mk2. --- index.md | 2 +- nodes/spatial/populate_solid.py | 117 +++++++++++++++--- old_nodes/populate_solid.py | 212 ++++++++++++++++++++++++++++++++ utils/field/probe.py | 1 + utils/surface/populate.py | 11 +- 5 files changed, 326 insertions(+), 17 deletions(-) create mode 100644 old_nodes/populate_solid.py diff --git a/index.md b/index.md index 2a1c9fdac..ea2209a21 100644 --- a/index.md +++ b/index.md @@ -314,7 +314,7 @@ SvHomogenousVectorField SvRandomPointsOnMesh SvPopulateSurfaceMk2Node - SvPopulateSolidNode + SvPopulateSolidMk2Node SvFieldRandomProbeMk3Node --- DelaunayTriangulation2DNode diff --git a/nodes/spatial/populate_solid.py b/nodes/spatial/populate_solid.py index 8ac24ecbf..b6969b9c1 100644 --- a/nodes/spatial/populate_solid.py +++ b/nodes/spatial/populate_solid.py @@ -12,7 +12,7 @@ from bpy.props import FloatProperty, StringProperty, BoolProperty, EnumProperty, from sverchok.core.socket_data import SvNoDataError from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, ensure_nesting_level, zip_long_repeat, throttle_and_update_node, repeat_last_for_length +from sverchok.data_structure import updateNode, ensure_nesting_level, zip_long_repeat, throttle_and_update_node, repeat_last_for_length, get_data_nesting_level from sverchok.utils.field.scalar import SvScalarField from sverchok.utils.field.probe import field_random_probe from sverchok.utils.surface.populate import populate_surface @@ -21,17 +21,17 @@ from sverchok.dependencies import FreeCAD from sverchok.utils.dummy_nodes import add_dummy if FreeCAD is None: - add_dummy('SvPopulateSolidNode', 'Populate Solid', 'FreeCAD') + add_dummy('SvPopulateSolidMk2Node', 'Populate Solid', 'FreeCAD') else: from FreeCAD import Base import Part -class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): +class SvPopulateSolidMk2Node(bpy.types.Node, SverchCustomTreeNode): """ Triggers: Populate Solid Tooltip: Generate random points within solid body """ - bl_idname = 'SvPopulateSolidNode' + bl_idname = 'SvPopulateSolidMk2Node' bl_label = 'Populate Solid' bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_POPULATE_SOLID' @@ -41,6 +41,9 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): self.inputs['FieldMin'].hide_safe = self.proportional != True self.inputs['FieldMax'].hide_safe = self.proportional != True self.inputs['FaceMask'].hide_safe = self.gen_mode != 'SURFACE' + self.inputs['RadiusField'].hide_safe = self.distance_mode != 'FIELD' + self.inputs['MinDistance'].hide_safe = self.distance_mode != 'CONST' + self.outputs['Radiuses'].hide_safe = self.distance_mode != 'FIELD' modes = [ ('VOLUME', "Volume", "Generate points inside solid body", 0), @@ -100,11 +103,39 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): default=True, 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) + + flat_output : BoolProperty( + name = "Flat output", + description = "If checked, generate one flat list of vertices for all input solids; otherwise, generate a separate list of vertices for each solid", + default = False, + update = updateNode) + def draw_buttons(self, context, layout): layout.prop(self, "gen_mode", text='Mode') + layout.prop(self, 'distance_mode') layout.prop(self, "proportional") if self.gen_mode == 'VOLUME': layout.prop(self, "in_surface") + if self.distance_mode == 'FIELD': + layout.prop(self, 'random_radius') + layout.prop(self, "flat_output") def draw_buttons_ext(self, context, layout): self.draw_buttons(context, layout) @@ -115,17 +146,20 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): 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', 'FaceMask') self.inputs.new('SvStringsSocket', 'Seed').prop_name = 'seed' self.outputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvStringsSocket', "Radiuses") + self.update_sockets(context) def get_tolerance(self): return 10**(-self.accuracy) - def generate_volume(self, solid, field, count, min_r, threshold, field_min, field_max, seed): + def generate_volume(self, solid, field, count, min_r, radius_field, threshold, field_min, field_max): def check(vert): point = Base.Vector(vert) return solid.isInside(point, self.get_tolerance(), self.in_surface) @@ -133,7 +167,11 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): box = solid.BoundBox bbox = ((box.XMin, box.YMin, box.ZMin), (box.XMax, box.YMax, box.ZMax)) - return field_random_probe(field, bbox, count, threshold, self.proportional, field_min, field_max, min_r, seed, predicate=check) + return field_random_probe(field, bbox, count, threshold, + self.proportional, field_min, field_max, + min_r = min_r, min_r_field = radius_field, + random_radius = self.random_radius, + seed = None, predicate=check) def distribute_faces(self, faces, total_count): points_per_face = [0 for _ in range(len(faces))] @@ -143,11 +181,13 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): points_per_face[i] += 1 return points_per_face - def generate_surface(self, solid, field, count, min_r, threshold, field_min, field_max, mask, seed): + def generate_surface(self, solid, field, count, min_r, radius_field, threshold, field_min, field_max, mask): counts = self.distribute_faces(solid.Faces, count) new_verts = [] + new_radiuses = [] mask = repeat_last_for_length(mask, len(solid.Faces)) counts = repeat_last_for_length(counts, len(solid.Faces)) + done_spheres = [] for face, ok, cnt in zip(solid.Faces, mask, counts): if not ok: continue @@ -158,9 +198,16 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): surface = SvSolidFaceSurface(face) - _, face_verts = populate_surface(surface, field, cnt, threshold, self.proportional, field_min, field_max, min_r, seed, predicate=check) + _, face_verts, radiuses = populate_surface(surface, field, cnt, threshold, + self.proportional, field_min, field_max, + min_r = min_r, min_r_field = radius_field, + random_radius = self.random_radius, + avoid_spheres = done_spheres, + seed = None, predicate=check) + done_spheres.extend(list(zip(face_verts, radiuses))) new_verts.extend(face_verts) - return new_verts + new_radiuses.extend(radiuses) + return new_verts, new_radiuses def process(self): if not any(socket.is_linked for socket in self.outputs): @@ -178,10 +225,26 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): field_max_s = self.inputs['FieldMax'].sv_get() mask_s = self.inputs['FaceMask'].sv_get(default=[[[True]]]) 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(solid_s, data_types=(Part.Shape,)) + nested_solid = input_level > 1 solid_s = ensure_nesting_level(solid_s, 2, data_types=(Part.Shape,)) 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) @@ -190,23 +253,47 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): mask_s = ensure_nesting_level(mask_s, 3) seed_s = ensure_nesting_level(seed_s, 2) + nested_output = nested_solid or nested_field or nested_radius + verts_out = [] - inputs = zip_long_repeat(solid_s, fields_s, count_s, min_r_s, threshold_s, field_min_s, field_max_s, mask_s, seed_s) + radius_out = [] + inputs = zip_long_repeat(solid_s, fields_s, count_s, min_r_s, radius_s, threshold_s, field_min_s, field_max_s, mask_s, seed_s) for objects in inputs: - for solid, field, count, min_r, threshold, field_min, field_max, mask, seed in zip_long_repeat(*objects): + new_verts = [] + new_radius = [] + for solid, field, count, min_r, radius, threshold, field_min, field_max, mask, seed in zip_long_repeat(*objects): + if seed == 0: + seed = 12345 + random.seed(seed) + if self.distance_mode == 'FIELD': + min_r = 0 if self.gen_mode == 'VOLUME': - new_verts = self.generate_volume(solid, field, count, min_r, threshold, field_min, field_max, seed) + verts, radiuses = self.generate_volume(solid, field, count, min_r, radius, threshold, field_min, field_max) else: - new_verts = self.generate_surface(solid, field, count, min_r, threshold, field_min, field_max, mask, seed) + verts, radiuses = self.generate_surface(solid, field, count, min_r, radius, threshold, field_min, field_max, mask) + + if self.flat_output: + new_verts.extend(verts) + new_radius.extend(radiuses) + else: + new_verts.append(verts) + new_radius.append(radiuses) + + if nested_output: verts_out.append(new_verts) + radius_out.append(new_radius) + else: + verts_out.extend(new_verts) + radius_out.extend(new_radius) self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Radiuses'].sv_set(radius_out) def register(): if FreeCAD is not None: - bpy.utils.register_class(SvPopulateSolidNode) + bpy.utils.register_class(SvPopulateSolidMk2Node) def unregister(): if FreeCAD is not None: - bpy.utils.unregister_class(SvPopulateSolidNode) + bpy.utils.unregister_class(SvPopulateSolidMk2Node) diff --git a/old_nodes/populate_solid.py b/old_nodes/populate_solid.py new file mode 100644 index 000000000..8ac24ecbf --- /dev/null +++ b/old_nodes/populate_solid.py @@ -0,0 +1,212 @@ +# 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 random + +import bpy +from bpy.props import FloatProperty, StringProperty, BoolProperty, EnumProperty, IntProperty + +from sverchok.core.socket_data import SvNoDataError +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, ensure_nesting_level, zip_long_repeat, throttle_and_update_node, repeat_last_for_length +from sverchok.utils.field.scalar import SvScalarField +from sverchok.utils.field.probe import field_random_probe +from sverchok.utils.surface.populate import populate_surface +from sverchok.utils.surface.freecad import SvSolidFaceSurface +from sverchok.dependencies import FreeCAD +from sverchok.utils.dummy_nodes import add_dummy + +if FreeCAD is None: + add_dummy('SvPopulateSolidNode', 'Populate Solid', 'FreeCAD') +else: + from FreeCAD import Base + import Part + +class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Populate Solid + Tooltip: Generate random points within solid body + """ + bl_idname = 'SvPopulateSolidNode' + bl_label = 'Populate Solid' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_POPULATE_SOLID' + + @throttle_and_update_node + def update_sockets(self, context): + self.inputs['FieldMin'].hide_safe = self.proportional != True + self.inputs['FieldMax'].hide_safe = self.proportional != True + self.inputs['FaceMask'].hide_safe = self.gen_mode != 'SURFACE' + + modes = [ + ('VOLUME', "Volume", "Generate points inside solid body", 0), + ('SURFACE', "Surface", "Generate points on the surface of the body", 1) + ] + + gen_mode : EnumProperty( + name = "Generation mode", + items = modes, + default = 'VOLUME', + update = update_sockets) + + threshold : FloatProperty( + name = "Threshold", + default = 0.5, + update = updateNode) + + field_min : FloatProperty( + name = "Field Minimum", + default = 0.0, + update = updateNode) + + field_max : FloatProperty( + name = "Field Maximum", + default = 1.0, + update = updateNode) + + seed: IntProperty(default=0, name='Seed', update=updateNode) + + count : IntProperty( + name = "Count", + default = 50, + min = 1, + update = updateNode) + + proportional : BoolProperty( + name = "Proportional", + default = False, + 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) + + accuracy: IntProperty( + name="Accuracy", + default=5, + min=1, + update=updateNode) + + in_surface: BoolProperty( + name="Accept in surface", + description="Accept point if it is over solid surface", + default=True, + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "gen_mode", text='Mode') + layout.prop(self, "proportional") + if self.gen_mode == 'VOLUME': + layout.prop(self, "in_surface") + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, "accuracy") + + def sv_init(self, context): + self.inputs.new('SvSolidSocket', "Solid") + 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('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', 'FaceMask') + self.inputs.new('SvStringsSocket', 'Seed').prop_name = 'seed' + self.outputs.new('SvVerticesSocket', "Vertices") + + def get_tolerance(self): + return 10**(-self.accuracy) + + def generate_volume(self, solid, field, count, min_r, threshold, field_min, field_max, seed): + def check(vert): + point = Base.Vector(vert) + return solid.isInside(point, self.get_tolerance(), self.in_surface) + + box = solid.BoundBox + bbox = ((box.XMin, box.YMin, box.ZMin), (box.XMax, box.YMax, box.ZMax)) + + return field_random_probe(field, bbox, count, threshold, self.proportional, field_min, field_max, min_r, seed, predicate=check) + + def distribute_faces(self, faces, total_count): + points_per_face = [0 for _ in range(len(faces))] + areas = [face.Area for face in faces] + chosen_faces = random.choices(range(len(faces)), weights=areas, k=total_count) + for i in chosen_faces: + points_per_face[i] += 1 + return points_per_face + + def generate_surface(self, solid, field, count, min_r, threshold, field_min, field_max, mask, seed): + counts = self.distribute_faces(solid.Faces, count) + new_verts = [] + mask = repeat_last_for_length(mask, len(solid.Faces)) + counts = repeat_last_for_length(counts, len(solid.Faces)) + for face, ok, cnt in zip(solid.Faces, mask, counts): + if not ok: + continue + + def check(uv, vert): + point = Base.Vector(vert) + return face.isInside(point, self.get_tolerance(), True) + + surface = SvSolidFaceSurface(face) + + _, face_verts = populate_surface(surface, field, cnt, threshold, self.proportional, field_min, field_max, min_r, seed, predicate=check) + new_verts.extend(face_verts) + return new_verts + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + if self.proportional and not self.inputs['Field'].is_linked: + raise SvNoDataError(socket=self.inputs['Field'], node=self) + + solid_s = self.inputs['Solid'].sv_get() + 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() + mask_s = self.inputs['FaceMask'].sv_get(default=[[[True]]]) + seed_s = self.inputs['Seed'].sv_get() + + solid_s = ensure_nesting_level(solid_s, 2, data_types=(Part.Shape,)) + if self.inputs['Field'].is_linked: + fields_s = ensure_nesting_level(fields_s, 2, data_types=(SvScalarField,)) + 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) + mask_s = ensure_nesting_level(mask_s, 3) + seed_s = ensure_nesting_level(seed_s, 2) + + verts_out = [] + inputs = zip_long_repeat(solid_s, fields_s, count_s, min_r_s, threshold_s, field_min_s, field_max_s, mask_s, seed_s) + for objects in inputs: + for solid, field, count, min_r, threshold, field_min, field_max, mask, seed in zip_long_repeat(*objects): + if self.gen_mode == 'VOLUME': + new_verts = self.generate_volume(solid, field, count, min_r, threshold, field_min, field_max, seed) + else: + new_verts = self.generate_surface(solid, field, count, min_r, threshold, field_min, field_max, mask, seed) + verts_out.append(new_verts) + + self.outputs['Vertices'].sv_set(verts_out) + +def register(): + if FreeCAD is not None: + bpy.utils.register_class(SvPopulateSolidNode) + +def unregister(): + if FreeCAD is not None: + bpy.utils.unregister_class(SvPopulateSolidNode) + diff --git a/utils/field/probe.py b/utils/field/probe.py index 59a6cad9c..617c77ff5 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -120,6 +120,7 @@ def field_random_probe(field, bbox, count, 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]) diff --git a/utils/surface/populate.py b/utils/surface/populate.py index 7bb7e38f5..3481cadba 100644 --- a/utils/surface/populate.py +++ b/utils/surface/populate.py @@ -52,6 +52,7 @@ def populate_surface(surface, field, count, threshold, proportional=False, field_min=None, field_max=None, min_r=0, min_r_field = None, random_radius = False, + avoid_spheres = None, seed=0, predicate=None): """ Generate random points on the surface, with distribution controlled (optionally) by scalar field. @@ -84,6 +85,13 @@ def populate_surface(surface, field, count, threshold, u_min, u_max = surface.get_u_min(), surface.get_u_max() v_min, v_max = surface.get_v_min(), surface.get_v_max() + 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 = [] + if seed == 0: seed = 12345 if seed is not None: @@ -144,6 +152,7 @@ def populate_surface(surface, field, count, threshold, if min_r == 0 and min_r_field is None: good_verts = candidates good_uvs = candidate_uvs + 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]) @@ -154,7 +163,7 @@ def populate_surface(surface, field, count, threshold, for candidate_uv, candidate, min_r in zip(candidate_uvs, 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): + if _check_min_radius(candidate, old_points + generated_verts + good_verts, old_radiuses + generated_radiuses + good_radiuses, min_r): good_verts.append(candidate) good_uvs.append(candidate_uv) good_radiuses.append(min_r) -- GitLab From 4485a18d697e3ee6b0204fd000a002856d442b6e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 3 Dec 2020 22:19:26 +0500 Subject: [PATCH 04/39] Fixes. --- utils/surface/populate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/surface/populate.py b/utils/surface/populate.py index 3481cadba..698036f12 100644 --- a/utils/surface/populate.py +++ b/utils/surface/populate.py @@ -150,8 +150,8 @@ def populate_surface(surface, field, count, threshold, good_radiuses = [] if len(candidates) > 0: if min_r == 0 and min_r_field is None: - good_verts = candidates - good_uvs = candidate_uvs + good_verts = candidates.tolist() + good_uvs = candidate_uvs.tolist() 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]) -- GitLab From fe2e8b564734d8d0f2a46a8e90e27d5342c2ba47 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 4 Dec 2020 00:00:47 +0500 Subject: [PATCH 05/39] Fixes. --- utils/surface/populate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/surface/populate.py b/utils/surface/populate.py index 698036f12..b3c99552c 100644 --- a/utils/surface/populate.py +++ b/utils/surface/populate.py @@ -171,10 +171,11 @@ def populate_surface(surface, field, count, threshold, good_verts = [] good_uvs = [] for candidate_uv, candidate in zip(candidate_uvs, candidates): - distance_ok = _check_min_distance(candidate, generated_verts + good_verts, min_r) + distance_ok = _check_min_distance(candidate, old_points + generated_verts + good_verts, min_r) if distance_ok: good_verts.append(tuple(candidate)) good_uvs.append(tuple(candidate_uv)) + good_radiuses.append(0) if predicate is not None: results = [(uv, vert, radius) for uv, vert, radius in zip(good_uvs, good_verts, good_radiuses) if predicate(uv, vert)] -- GitLab From 18f28755e170abb70561ede9083eb15c323903cd Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 4 Dec 2020 00:56:39 +0500 Subject: [PATCH 06/39] Fixes. --- utils/voronoi3d.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index 3d92e7a0c..756926be9 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -186,7 +186,7 @@ def voronoi_on_mesh(verts, faces, sites, thickness, clip_inner=True, clip_outer= do_clip = do_clip, clipping = clipping, make_regions = make_regions) -def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True): +def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True, predicate_plus=None, predicate_minus=None): k = 0.5*thickness result = [] for pt in pts: @@ -200,15 +200,18 @@ def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True): plus_pt = projection + k*normal minus_pt = projection - k*normal if add_plus: - result.append(tuple(plus_pt)) + if predicate_plus is None or predicate_plus(plus_pt): + result.append(tuple(plus_pt)) if add_minus: - result.append(tuple(minus_pt)) + if predicate_minus is None or predicate_minus(minus_pt): + result.append(tuple(minus_pt)) return result def voronoi_on_solid_surface(solid, sites, thickness, clip_inner=True, clip_outer=True, skip_added = True, do_clip=True, clipping=1.0, + tolerance = 1e-4, make_regions=True): npoints = len(sites) @@ -217,10 +220,13 @@ def voronoi_on_solid_surface(solid, sites, thickness, else: shell = Part.Shell(solid.Faces) + def check(pt): + return solid.isInside(pt, tolerance, False) + all_points = sites if clip_inner or clip_outer: all_points.extend(project_solid_normals(shell, sites, thickness, - add_plus=clip_outer, add_minus=clip_inner)) + add_plus=clip_outer, add_minus=clip_inner, predicate_minus=check)) return voronoi3d_layer(npoints, all_points, make_regions = make_regions, -- GitLab From 9e7169e53f36266e5fe53d33be785f161e8b7d38 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 5 Dec 2020 17:58:30 +0500 Subject: [PATCH 07/39] Add replacements. --- old_nodes/populate_solid.py | 2 ++ old_nodes/populate_surface.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/old_nodes/populate_solid.py b/old_nodes/populate_solid.py index 8ac24ecbf..118b2e89a 100644 --- a/old_nodes/populate_solid.py +++ b/old_nodes/populate_solid.py @@ -36,6 +36,8 @@ class SvPopulateSolidNode(bpy.types.Node, SverchCustomTreeNode): bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_POPULATE_SOLID' + replacement_nodes = [('SvPopulateSolidMk2Node', None, None)] + @throttle_and_update_node def update_sockets(self, context): self.inputs['FieldMin'].hide_safe = self.proportional != True diff --git a/old_nodes/populate_surface.py b/old_nodes/populate_surface.py index 05393ba9b..4732b7997 100644 --- a/old_nodes/populate_surface.py +++ b/old_nodes/populate_surface.py @@ -29,6 +29,8 @@ class SvPopulateSurfaceNode(bpy.types.Node, SverchCustomTreeNode): bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_POPULATE_SURFACE' + replacement_nodes = [('SvPopulateSurfaceMk2Node', None, None)] + threshold : FloatProperty( name = "Threshold", default = 0.5, -- GitLab From 3a6adea50678cff3340f587a04a5dcd55a0fd658 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 11 Dec 2020 21:36:51 +0500 Subject: [PATCH 08/39] "voronoi on mesh" API. --- utils/voronoi3d.py | 115 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 25 deletions(-) diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index 756926be9..3bdb90e4d 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -26,7 +26,7 @@ from mathutils.bvhtree import BVHTree from sverchok.utils.sv_mesh_utils import mask_vertices, polygons_to_edges from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh, bmesh_clip -from sverchok.utils.geom import calc_bounds, bounding_sphere +from sverchok.utils.geom import calc_bounds, bounding_sphere, PlaneEquation from sverchok.utils.math import project_to_sphere, weighted_center from sverchok.dependencies import scipy, FreeCAD @@ -159,32 +159,97 @@ def calc_bvh_projections(bvh, sites): projections.append(loc) return np.array(projections) -def voronoi_on_mesh_impl(bvh, sites, thickness, clip_inner=True, clip_outer=True, do_clip=True, clipping=1.0, make_regions=True): - npoints = len(sites) - - if clip_inner or clip_outer: - normals = calc_bvh_normals(bvh, sites) - k = 0.5*thickness - sites = np.array(sites) - all_points = sites.tolist() - if clip_outer: - plus_points = sites + k*normals - all_points.extend(plus_points.tolist()) - if clip_inner: - minus_points = sites - k*normals - all_points.extend(minus_points.tolist()) - - return voronoi3d_layer(npoints, all_points, - make_regions = make_regions, - do_clip = do_clip, - clipping = clipping) +def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True): + + def get_ridges_per_site(voronoi): + result = defaultdict(list) + for ridge_idx in range(len(voronoi.ridge_points)): + site1_idx, site2_idx = tuple(voronoi.ridge_points[ridge_idx]) + site1 = voronoi.points[site1_idx] + site2 = voronoi.points[site2_idx] + middle = (site1 + site2) * 0.5 + normal = site2 - site1 + plane = PlaneEquation.from_normal_and_point(normal, middle) + result[site1_idx].append(plane) + result[site2_idx].append(plane) + return result -def voronoi_on_mesh(verts, faces, sites, thickness, clip_inner=True, clip_outer=True, do_clip=True, clipping=1.0, make_regions=True): + def cut_cell(verts, faces, planes, site): + src_mesh = bmesh_from_pydata(verts, [], faces, normal_update=True) + for plane in planes: + geom_in = src_mesh.verts[:] + src_mesh.edges[:] + src_mesh.faces[:] + + plane_co = plane.projection_of_point(site) + plane_no = plane.normal + if plane.side_of_point(site) > 0: + plane_no = - plane_no + + plane_co = plane_co - 0.5 * spacing * plane_no.normalized() + #print(f"Plane co {plane_co}, no {plane_no}") + res = bmesh.ops.bisect_plane( + src_mesh, geom=geom_in, dist=0.00001, + plane_co = plane_co, + plane_no = plane_no, + use_snap_center = False, + clear_outer = True, + clear_inner = False + ) + + if fill: + surround = [e for e in res['geom_cut'] if isinstance(e, bmesh.types.BMEdge)] + fres = bmesh.ops.edgenet_prepare(src_mesh, edges=surround) + bmesh.ops.edgeloop_fill(src_mesh, edges=fres['edges']) + + return pydata_from_bmesh(src_mesh) + + verts_out = [] + edges_out = [] + faces_out = [] + + voronoi = Voronoi(np.array(sites)) + ridges_per_site = get_ridges_per_site(voronoi) + for site_idx in range(len(sites)): + new_verts, new_edges, new_faces = cut_cell(verts, faces, ridges_per_site[site_idx], sites[site_idx]) + if new_verts: + verts_out.append(new_verts) + edges_out.append(new_edges) + faces_out.append(new_faces) + + return verts_out, edges_out, faces_out + +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'): bvh = BVHTree.FromPolygons(verts, faces) - return voronoi_on_mesh_impl(bvh, sites, thickness, - clip_inner = clip_inner, clip_outer = clip_outer, - do_clip = do_clip, clipping = clipping, - make_regions = make_regions) + npoints = len(sites) + + if mode in {'REGIONS', 'RIDGES'}: + if clip_inner or clip_outer: + normals = calc_bvh_normals(bvh, sites) + k = 0.5*thickness + sites = np.array(sites) + all_points = sites.tolist() + if clip_outer: + plus_points = sites + k*normals + all_points.extend(plus_points.tolist()) + if clip_inner: + minus_points = sites - k*normals + all_points.extend(minus_points.tolist()) + + return voronoi3d_layer(npoints, all_points, + make_regions = (mode == 'REGIONS'), + do_clip = do_clip, + clipping = clipping) + + else: # VOLUME, SURFACE + all_points = sites + if do_clip: + normals = calc_bvh_normals(bvh, sites) + plus_points = sites + clipping*normals + all_points.extend(plus_points.tolist()) + return voronoi_on_mesh_bmesh(verts, faces, all_points, + spacing = spacing, fill = (mode == 'VOLUME')) def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True, predicate_plus=None, predicate_minus=None): k = 0.5*thickness -- GitLab From d418f9a820c193877aaba256c8492f46f2281a84 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 11 Dec 2020 22:34:06 +0500 Subject: [PATCH 09/39] Move "voronoi on mesh" from Sverchok-Extra. --- index.md | 1 + nodes/spatial/voronoi_on_mesh.py | 212 +++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 nodes/spatial/voronoi_on_mesh.py diff --git a/index.md b/index.md index ea2209a21..93c9d0e94 100644 --- a/index.md +++ b/index.md @@ -325,6 +325,7 @@ SvExVoronoi3DNode SvExVoronoiSphereNode SvVoronoiOnSurfaceNode + SvVoronoiOnMeshNode --- SvLloyd2dNode SvLloyd3dNode diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py new file mode 100644 index 000000000..59f603d8c --- /dev/null +++ b/nodes/spatial/voronoi_on_mesh.py @@ -0,0 +1,212 @@ +# ##### 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 ##### + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty +import bmesh +from mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_update_node, ensure_nesting_level, get_data_nesting_level +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 +from sverchok.utils.dummy_nodes import add_dummy +from sverchok.dependencies import scipy + +if scipy is None: + add_dummy('SvVoronoiOnMeshNode', "Voronoi on Mesh", 'scipy') + +class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Voronoi Mesh + Tooltip: Generate Voronoi diagram on the surface of a mesh object + """ + bl_idname = 'SvVoronoiOnMeshNode' + bl_label = 'Voronoi on Mesh' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VORONOI' + + modes = [ + ('VOLUME', "Split Volume", "Split volume of the mesh into regions of Voronoi diagram", 0), + ('SURFACE', "Split Surface", "Split the surface of the mesh into regions of Vornoi diagram", 1), + ('RIDGES', "Ridges near Surface", "Generate ridges of 3D Voronoi diagram near the surface of the mesh", 2), + ('REGIONS', "Regions near Surface", "Generate regions of 3D Voronoi diagram near the surface of the mesh", 3) + ] + + thickness : FloatProperty( + name = "Thickness", + default = 1.0, + min = 0.0, + update=updateNode) + + spacing : FloatProperty( + name = "Spacing", + default = 0.0, + min = 0.0, + update=updateNode) + + normals : BoolProperty( + name = "Correct normals", + default = True, + update = updateNode) + + @throttle_and_update_node + def update_sockets(self, context): + self.inputs['Clipping'].hide_safe = not self.do_clip + self.inputs['Thickness'].hide_safe = self.mode not in {'RIDGES', 'REGIONS'} + self.inputs['Spacing'].hide_safe = self.mode not in {'VOLUME', 'SURFACE'} + + mode : EnumProperty( + name = "Mode", + items = modes, + default = 'VOLUME', + update = update_sockets) + + do_clip : BoolProperty( + name = "Clip Box", + default = True, + update = update_sockets) + + clip_inner : BoolProperty( + name = "Clip Inner", + default = True, + update = updateNode) + + clip_outer : BoolProperty( + name = "Clip Outer", + default = True, + update = updateNode) + + clipping : FloatProperty( + name = "Clipping", + default = 1.0, + min = 0.0, + update = updateNode) + + join_modes = [ + ('FLAT', "Flat list", "Output a single flat list of mesh objects (Voronoi diagram ridges / regions) for all input meshes", 0), + ('SEPARATE', "Separate lists", "Output a separate list of mesh objects (Voronoi diagram ridges / regions) for each input mesh", 1), + ('JOIN', "Join meshes", "Output one mesh, joined from ridges / edges of Voronoi diagram, for each input mesh", 2) + ] + + join_mode : EnumProperty( + name = "Output mode", + items = join_modes, + default = 'FLAT', + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', 'Vertices') + self.inputs.new('SvStringsSocket', 'Faces') + self.inputs.new('SvVerticesSocket', "Sites") + self.inputs.new('SvStringsSocket', 'Thickness').prop_name = 'thickness' + self.inputs.new('SvStringsSocket', 'Spacing').prop_name = 'spacing' + self.inputs.new('SvStringsSocket', "Clipping").prop_name = 'clipping' + self.outputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvStringsSocket', "Edges") + self.outputs.new('SvStringsSocket', "Faces") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.label(text="Mode:") + layout.prop(self, "mode", text='') + if self.mode in {'REGIONS', 'RIDGES'}: + row = layout.row(align=True) + row.prop(self, 'clip_inner', toggle=True) + row.prop(self, 'clip_outer', toggle=True) + layout.prop(self, 'do_clip', toggle=True) + layout.prop(self, 'normals') + layout.label(text='Output:') + layout.prop(self, 'join_mode', text='') + + def process(self): + + if not any(socket.is_linked for socket in self.outputs): + return + + verts_in = self.inputs['Vertices'].sv_get() + faces_in = self.inputs['Faces'].sv_get() + sites_in = self.inputs['Sites'].sv_get() + thickness_in = self.inputs['Thickness'].sv_get() + spacing_in = self.inputs['Spacing'].sv_get() + clipping_in = self.inputs['Clipping'].sv_get() + + verts_in = ensure_nesting_level(verts_in, 4) + input_level = get_data_nesting_level(sites_in) + sites_in = ensure_nesting_level(sites_in, 4) + faces_in = ensure_nesting_level(faces_in, 4) + thickness_in = ensure_nesting_level(thickness_in, 2) + spacing_in = ensure_nesting_level(spacing_in, 2) + clipping_in = ensure_nesting_level(clipping_in, 2) + + nested_output = input_level > 3 + + verts_out = [] + edges_out = [] + faces_out = [] + for params in zip_long_repeat(verts_in, faces_in, sites_in, thickness_in, spacing_in, clipping_in): + new_verts = [] + new_edges = [] + new_faces = [] + for verts, faces, sites, thickness, spacing, clipping in zip_long_repeat(*params): + verts, edges, faces = voronoi_on_mesh(verts, faces, sites, thickness, + spacing = spacing, + clip_inner = self.clip_inner, clip_outer = self.clip_outer, + do_clip=self.do_clip, clipping=clipping, + mode = self.mode) + if self.normals: + verts, edges, faces = recalc_normals(verts, edges, faces, loop=True) + + if self.join_mode == 'FLAT': + new_verts.extend(verts) + new_edges.extend(edges) + new_faces.extend(faces) + elif self.join_mode == 'SEPARATE': + new_verts.append(verts) + new_edges.append(edges) + new_faces.append(faces) + else: # JOIN + verts, edges, faces = mesh_join(verts, edges, faces) + new_verts.append(verts) + new_edges.append(edges) + new_faces.append(faces) + + if nested_output: + verts_out.append(new_verts) + edges_out.append(new_edges) + faces_out.append(new_faces) + else: + verts_out.extend(new_verts) + edges_out.extend(new_edges) + faces_out.extend(new_faces) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Edges'].sv_set(edges_out) + self.outputs['Faces'].sv_set(faces_out) + +def register(): + if scipy is not None: + bpy.utils.register_class(SvVoronoiOnMeshNode) + +def unregister(): + if scipy is not None: + bpy.utils.unregister_class(SvVoronoiOnMeshNode) + -- GitLab From bbba3d4d073b79e5ba64358ff3349099267ab694 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 11 Dec 2020 23:28:54 +0500 Subject: [PATCH 10/39] Minor update. --- nodes/spatial/voronoi_on_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py index 59f603d8c..0b93562f0 100644 --- a/nodes/spatial/voronoi_on_mesh.py +++ b/nodes/spatial/voronoi_on_mesh.py @@ -134,7 +134,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): row.prop(self, 'clip_outer', toggle=True) layout.prop(self, 'do_clip', toggle=True) layout.prop(self, 'normals') - layout.label(text='Output:') + layout.label(text='Output nesting:') layout.prop(self, 'join_mode', text='') def process(self): -- GitLab From 424ffddc4b07f0ff86aa1ef25ae279d0b8a0fda7 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 12 Dec 2020 11:20:11 +0500 Subject: [PATCH 11/39] Voronoi on mesh: comment out doubtful modes. --- nodes/spatial/voronoi_on_mesh.py | 56 ++++++++++++++++---------------- utils/voronoi3d.py | 32 +++++++++++++----- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py index 0b93562f0..48f55004c 100644 --- a/nodes/spatial/voronoi_on_mesh.py +++ b/nodes/spatial/voronoi_on_mesh.py @@ -47,15 +47,15 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): modes = [ ('VOLUME', "Split Volume", "Split volume of the mesh into regions of Voronoi diagram", 0), ('SURFACE', "Split Surface", "Split the surface of the mesh into regions of Vornoi diagram", 1), - ('RIDGES', "Ridges near Surface", "Generate ridges of 3D Voronoi diagram near the surface of the mesh", 2), - ('REGIONS', "Regions near Surface", "Generate regions of 3D Voronoi diagram near the surface of the mesh", 3) + #('RIDGES', "Ridges near Surface", "Generate ridges of 3D Voronoi diagram near the surface of the mesh", 2), + #('REGIONS', "Regions near Surface", "Generate regions of 3D Voronoi diagram near the surface of the mesh", 3) ] - thickness : FloatProperty( - name = "Thickness", - default = 1.0, - min = 0.0, - update=updateNode) +# thickness : FloatProperty( +# name = "Thickness", +# default = 1.0, +# min = 0.0, +# update=updateNode) spacing : FloatProperty( name = "Spacing", @@ -71,7 +71,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): @throttle_and_update_node def update_sockets(self, context): self.inputs['Clipping'].hide_safe = not self.do_clip - self.inputs['Thickness'].hide_safe = self.mode not in {'RIDGES', 'REGIONS'} + #self.inputs['Thickness'].hide_safe = self.mode not in {'RIDGES', 'REGIONS'} self.inputs['Spacing'].hide_safe = self.mode not in {'VOLUME', 'SURFACE'} mode : EnumProperty( @@ -85,15 +85,15 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): default = True, update = update_sockets) - clip_inner : BoolProperty( - name = "Clip Inner", - default = True, - update = updateNode) - - clip_outer : BoolProperty( - name = "Clip Outer", - default = True, - update = updateNode) +# clip_inner : BoolProperty( +# name = "Clip Inner", +# default = True, +# update = updateNode) +# +# clip_outer : BoolProperty( +# name = "Clip Outer", +# default = True, +# update = updateNode) clipping : FloatProperty( name = "Clipping", @@ -117,7 +117,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvVerticesSocket', 'Vertices') self.inputs.new('SvStringsSocket', 'Faces') self.inputs.new('SvVerticesSocket', "Sites") - self.inputs.new('SvStringsSocket', 'Thickness').prop_name = 'thickness' +# self.inputs.new('SvStringsSocket', 'Thickness').prop_name = 'thickness' self.inputs.new('SvStringsSocket', 'Spacing').prop_name = 'spacing' self.inputs.new('SvStringsSocket', "Clipping").prop_name = 'clipping' self.outputs.new('SvVerticesSocket', "Vertices") @@ -128,10 +128,10 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons(self, context, layout): layout.label(text="Mode:") layout.prop(self, "mode", text='') - if self.mode in {'REGIONS', 'RIDGES'}: - row = layout.row(align=True) - row.prop(self, 'clip_inner', toggle=True) - row.prop(self, 'clip_outer', toggle=True) +# if self.mode in {'REGIONS', 'RIDGES'}: +# row = layout.row(align=True) +# row.prop(self, 'clip_inner', toggle=True) +# row.prop(self, 'clip_outer', toggle=True) layout.prop(self, 'do_clip', toggle=True) layout.prop(self, 'normals') layout.label(text='Output nesting:') @@ -145,7 +145,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): verts_in = self.inputs['Vertices'].sv_get() faces_in = self.inputs['Faces'].sv_get() sites_in = self.inputs['Sites'].sv_get() - thickness_in = self.inputs['Thickness'].sv_get() + #thickness_in = self.inputs['Thickness'].sv_get() spacing_in = self.inputs['Spacing'].sv_get() clipping_in = self.inputs['Clipping'].sv_get() @@ -153,7 +153,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): input_level = get_data_nesting_level(sites_in) sites_in = ensure_nesting_level(sites_in, 4) faces_in = ensure_nesting_level(faces_in, 4) - thickness_in = ensure_nesting_level(thickness_in, 2) + #thickness_in = ensure_nesting_level(thickness_in, 2) spacing_in = ensure_nesting_level(spacing_in, 2) clipping_in = ensure_nesting_level(clipping_in, 2) @@ -162,14 +162,14 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): verts_out = [] edges_out = [] faces_out = [] - for params in zip_long_repeat(verts_in, faces_in, sites_in, thickness_in, spacing_in, clipping_in): + for params in zip_long_repeat(verts_in, faces_in, sites_in, spacing_in, clipping_in): new_verts = [] new_edges = [] new_faces = [] - for verts, faces, sites, thickness, spacing, clipping in zip_long_repeat(*params): - verts, edges, faces = voronoi_on_mesh(verts, faces, sites, thickness, + for verts, faces, sites, spacing, clipping in zip_long_repeat(*params): + verts, edges, faces = voronoi_on_mesh(verts, faces, sites, thickness=0, spacing = spacing, - clip_inner = self.clip_inner, clip_outer = self.clip_outer, + #clip_inner = self.clip_inner, clip_outer = self.clip_outer, do_clip=self.do_clip, clipping=clipping, mode = self.mode) if self.normals: diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index 3bdb90e4d..7f8451c11 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -18,6 +18,7 @@ import numpy as np from collections import defaultdict +import itertools import bpy import bmesh @@ -277,7 +278,7 @@ def voronoi_on_solid_surface(solid, sites, thickness, skip_added = True, do_clip=True, clipping=1.0, tolerance = 1e-4, - make_regions=True): + mode = 'REGIONS'): npoints = len(sites) if solid.Shells: @@ -289,15 +290,28 @@ def voronoi_on_solid_surface(solid, sites, thickness, return solid.isInside(pt, tolerance, False) all_points = sites - if clip_inner or clip_outer: - all_points.extend(project_solid_normals(shell, sites, thickness, - add_plus=clip_outer, add_minus=clip_inner, predicate_minus=check)) + if mode == 'FACES': + if do_clip: + x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(sites, clipping) + bounds = list(itertools.product([x_min,x_max], [y_min, y_max], [z_min, z_max])) + all_points.extend(bounds) +# all_points.extend(project_solid_normals(shell, sites, clipping, +# add_plus=True, add_minus=False)) + return voronoi3d_layer(npoints, all_points, + make_regions = True, + skip_added = False, + do_clip = False, + clipping = 0) + else: + if clip_inner or clip_outer: + all_points.extend(project_solid_normals(shell, sites, thickness, + add_plus=clip_outer, add_minus=clip_inner, predicate_minus=check)) - return voronoi3d_layer(npoints, all_points, - make_regions = make_regions, - skip_added = skip_added, - do_clip = do_clip, - clipping = clipping) + return voronoi3d_layer(npoints, all_points, + make_regions = (mode in {'REGIONS', 'MESH'}), + skip_added = skip_added, + do_clip = do_clip, + clipping = clipping) def lloyd_on_mesh(verts, faces, sites, thickness, n_iterations, weight_field=None): bvh = BVHTree.FromPolygons(verts, faces) -- GitLab From eaaa09337e4f1a96f75babff074890f941e3363e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 12 Dec 2020 20:59:15 +0500 Subject: [PATCH 12/39] Voronoi on mesh: remove "clipping" --- nodes/spatial/voronoi_on_mesh.py | 22 +----- utils/field/probe.py | 2 +- utils/geom.py | 9 +++ utils/voronoi3d.py | 124 ++++++++++++++++++++++--------- 4 files changed, 100 insertions(+), 57 deletions(-) diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py index 48f55004c..789c76618 100644 --- a/nodes/spatial/voronoi_on_mesh.py +++ b/nodes/spatial/voronoi_on_mesh.py @@ -70,7 +70,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): @throttle_and_update_node def update_sockets(self, context): - self.inputs['Clipping'].hide_safe = not self.do_clip #self.inputs['Thickness'].hide_safe = self.mode not in {'RIDGES', 'REGIONS'} self.inputs['Spacing'].hide_safe = self.mode not in {'VOLUME', 'SURFACE'} @@ -80,11 +79,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): default = 'VOLUME', update = update_sockets) - do_clip : BoolProperty( - name = "Clip Box", - default = True, - update = update_sockets) - # clip_inner : BoolProperty( # name = "Clip Inner", # default = True, @@ -95,12 +89,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): # default = True, # update = updateNode) - clipping : FloatProperty( - name = "Clipping", - default = 1.0, - min = 0.0, - update = updateNode) - join_modes = [ ('FLAT', "Flat list", "Output a single flat list of mesh objects (Voronoi diagram ridges / regions) for all input meshes", 0), ('SEPARATE', "Separate lists", "Output a separate list of mesh objects (Voronoi diagram ridges / regions) for each input mesh", 1), @@ -119,7 +107,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvVerticesSocket', "Sites") # self.inputs.new('SvStringsSocket', 'Thickness').prop_name = 'thickness' self.inputs.new('SvStringsSocket', 'Spacing').prop_name = 'spacing' - self.inputs.new('SvStringsSocket', "Clipping").prop_name = 'clipping' self.outputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvStringsSocket', "Edges") self.outputs.new('SvStringsSocket', "Faces") @@ -132,7 +119,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): # row = layout.row(align=True) # row.prop(self, 'clip_inner', toggle=True) # row.prop(self, 'clip_outer', toggle=True) - layout.prop(self, 'do_clip', toggle=True) layout.prop(self, 'normals') layout.label(text='Output nesting:') layout.prop(self, 'join_mode', text='') @@ -147,7 +133,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): sites_in = self.inputs['Sites'].sv_get() #thickness_in = self.inputs['Thickness'].sv_get() spacing_in = self.inputs['Spacing'].sv_get() - clipping_in = self.inputs['Clipping'].sv_get() verts_in = ensure_nesting_level(verts_in, 4) input_level = get_data_nesting_level(sites_in) @@ -155,22 +140,21 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): faces_in = ensure_nesting_level(faces_in, 4) #thickness_in = ensure_nesting_level(thickness_in, 2) spacing_in = ensure_nesting_level(spacing_in, 2) - clipping_in = ensure_nesting_level(clipping_in, 2) nested_output = input_level > 3 verts_out = [] edges_out = [] faces_out = [] - for params in zip_long_repeat(verts_in, faces_in, sites_in, spacing_in, clipping_in): + for params in zip_long_repeat(verts_in, faces_in, sites_in, spacing_in): new_verts = [] new_edges = [] new_faces = [] - for verts, faces, sites, spacing, clipping in zip_long_repeat(*params): + for verts, faces, sites, spacing in zip_long_repeat(*params): verts, edges, faces = voronoi_on_mesh(verts, faces, sites, thickness=0, spacing = spacing, #clip_inner = self.clip_inner, clip_outer = self.clip_outer, - do_clip=self.do_clip, clipping=clipping, + do_clip=True, clipping=None, mode = self.mode) if self.normals: verts, edges, faces = recalc_normals(verts, edges, faces, loop=True) diff --git a/utils/field/probe.py b/utils/field/probe.py index 617c77ff5..e2e2d3425 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -137,7 +137,7 @@ def field_random_probe(field, bbox, count, good_verts = [] for candidate in candidates: if _check_min_distance(candidate, generated_verts + good_verts, min_r): - good_verts.append(candidate.tolist()) + good_verts.append(candidate) if predicate is not None: pairs = [(vert, r) for vert, r in zip(good_verts, good_radiuses) if predicate(vert)] diff --git a/utils/geom.py b/utils/geom.py index 407d11420..c4e4a9081 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -2181,3 +2181,12 @@ def bounding_sphere(vertices, algorithm=TRIVIAL): radius = norms.max() return c, radius +def scale_relative(points, center, scale): + points = np.asarray(points) + center = np.asarray(center) + points -= center + + points = points * scale + + return (points + center).tolist() + diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index 7f8451c11..e12e8797c 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -38,6 +38,77 @@ if FreeCAD is not None: from FreeCAD import Base import Part +def voronoi3d_regions(sites, closed_only=True, recalc_normals=True, do_clip=False, clipping=1.0): + diagram = Voronoi(sites) + faces_per_site = defaultdict(list) + nsites = len(diagram.point_region) + nridges = len(diagram.ridge_points) + open_sites = set() + for ridge_idx in range(nridges): + site_idx_1, site_idx_2 = diagram.ridge_points[ridge_idx] + face = diagram.ridge_vertices[ridge_idx] + if -1 in face: + open_sites.add(site_idx_1) + open_sites.add(site_idx_2) + continue + faces_per_site[site_idx_1].append(face) + faces_per_site[site_idx_2].append(face) + + new_verts = [] + new_edges = [] + new_faces = [] + + for site_idx in sorted(faces_per_site.keys()): + if closed_only and site_idx in open_sites: + continue + done_verts = dict() + bm = bmesh.new() + for face in faces_per_site[site_idx]: + face_bm_verts = [] + for vertex_idx in face: + if vertex_idx not in done_verts: + bm_vert = bm.verts.new(diagram.vertices[vertex_idx]) + done_verts[vertex_idx] = bm_vert + else: + bm_vert = done_verts[vertex_idx] + face_bm_verts.append(bm_vert) + bm.faces.new(face_bm_verts) + bm.verts.index_update() + bm.verts.ensure_lookup_table() + bm.faces.index_update() + bm.edges.index_update() + + if closed_only and any (v.is_boundary for v in bm.verts): + bm.free() + continue + + if recalc_normals: + bm.normal_update() + bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) + + region_verts, region_edges, region_faces = pydata_from_bmesh(bm) + bm.free() + new_verts.append(region_verts) + new_edges.append(region_edges) + new_faces.append(region_faces) + + if do_clip: + verts_n, edges_n, faces_n = [], [], [] + bounds = calc_bounds(sites, clipping) + for verts_i, edges_i, faces_i in zip(new_verts, new_edges, new_faces): + bm = bmesh_from_pydata(verts_i, edges_i, faces_i) + bmesh_clip(bm, bounds, fill=True) + bm.normal_update() + bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) + verts_i, edges_i, faces_i = pydata_from_bmesh(bm) + bm.free() + verts_n.append(verts_i) + edges_n.append(edges_i) + faces_n.append(faces_i) + new_verts, new_edges, new_faces = verts_n, edges_n, faces_n + + return new_verts, new_edges, new_faces + def voronoi3d_layer(n_src_sites, all_sites, make_regions, do_clip, clipping, skip_added=True): diagram = Voronoi(all_sites) src_sites = all_sites[:n_src_sites] @@ -225,6 +296,10 @@ def voronoi_on_mesh(verts, faces, sites, thickness, bvh = BVHTree.FromPolygons(verts, faces) npoints = len(sites) + if clipping is None: + x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(verts) + clipping = max(x_max - x_min, y_max - y_min, z_max - z_min) / 2.0 + if mode in {'REGIONS', 'RIDGES'}: if clip_inner or clip_outer: normals = calc_bvh_normals(bvh, sites) @@ -273,45 +348,20 @@ def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True, result.append(tuple(minus_pt)) return result -def voronoi_on_solid_surface(solid, sites, thickness, - clip_inner=True, clip_outer=True, - skip_added = True, - do_clip=True, clipping=1.0, - tolerance = 1e-4, - mode = 'REGIONS'): - - npoints = len(sites) - if solid.Shells: - shell = solid.Shells[0] - else: - shell = Part.Shell(solid.Faces) - - def check(pt): - return solid.isInside(pt, tolerance, False) +def voronoi_on_solid(solid, sites, + do_clip=True, clipping=1.0): all_points = sites - if mode == 'FACES': - if do_clip: - x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(sites, clipping) - bounds = list(itertools.product([x_min,x_max], [y_min, y_max], [z_min, z_max])) - all_points.extend(bounds) -# all_points.extend(project_solid_normals(shell, sites, clipping, -# add_plus=True, add_minus=False)) - return voronoi3d_layer(npoints, all_points, - make_regions = True, - skip_added = False, - do_clip = False, - clipping = 0) - else: - if clip_inner or clip_outer: - all_points.extend(project_solid_normals(shell, sites, thickness, - add_plus=clip_outer, add_minus=clip_inner, predicate_minus=check)) - - return voronoi3d_layer(npoints, all_points, - make_regions = (mode in {'REGIONS', 'MESH'}), - skip_added = skip_added, - do_clip = do_clip, - clipping = clipping) + if do_clip: + box = solid.BoundBox + if clipping is None: + clipping = max(box.XLength, box.YLength, box.ZLength)/2.0 + x_min, x_max = box.XMin - clipping, box.XMax + clipping + y_min, y_max = box.YMin - clipping, box.YMax + clipping + z_min, z_max = box.ZMin - clipping, box.ZMax + clipping + bounds = list(itertools.product([x_min,x_max], [y_min, y_max], [z_min, z_max])) + all_points.extend(bounds) + return voronoi3d_regions(all_points, closed_only=True, do_clip=do_clip, clipping=clipping) def lloyd_on_mesh(verts, faces, sites, thickness, n_iterations, weight_field=None): bvh = BVHTree.FromPolygons(verts, faces) -- GitLab From df3d15f1a675d6de26ba6eca980b23092d033f1b Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 12 Dec 2020 21:01:44 +0500 Subject: [PATCH 13/39] Move "voronoi on solid" from Sverchok-Extra. --- index.md | 1 + nodes/spatial/voronoi_on_solid.py | 234 ++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 nodes/spatial/voronoi_on_solid.py diff --git a/index.md b/index.md index 93c9d0e94..276efbbb4 100644 --- a/index.md +++ b/index.md @@ -326,6 +326,7 @@ SvExVoronoiSphereNode SvVoronoiOnSurfaceNode SvVoronoiOnMeshNode + SvVoronoiOnSolidNode --- SvLloyd2dNode SvLloyd3dNode diff --git a/nodes/spatial/voronoi_on_solid.py b/nodes/spatial/voronoi_on_solid.py new file mode 100644 index 000000000..2413caed1 --- /dev/null +++ b/nodes/spatial/voronoi_on_solid.py @@ -0,0 +1,234 @@ +# ##### 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 ##### + +import numpy as np +from collections import defaultdict + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty +import bmesh +from mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_update_node, ensure_nesting_level, get_data_nesting_level +from sverchok.utils.sv_bmesh_utils import recalc_normals +from sverchok.utils.voronoi3d import voronoi_on_solid +from sverchok.utils.geom import scale_relative +from sverchok.utils.solid import svmesh_to_solid, SvSolidTopology +from sverchok.utils.surface.freecad import SvSolidFaceSurface +from sverchok.utils.dummy_nodes import add_dummy +from sverchok.dependencies import scipy, FreeCAD + +if scipy is None or FreeCAD is None: + add_dummy('SvVoronoiOnSolidNode', "Voronoi on Solid", 'scipy and FreeCAD') + +if FreeCAD is not None: + import Part + +def mesh_from_faces(fragments): + verts = [(v.X, v.Y, v.Z) for v in fragments.Vertexes] + + all_fc_verts = {SvSolidTopology.Item(v) : i for i, v in enumerate(fragments.Vertexes)} + def find_vertex(v): + #for i, fc_vert in enumerate(fragments.Vertexes): + # if v.isSame(fc_vert): + # return i + #return None + return all_fc_verts[SvSolidTopology.Item(v)] + + edges = [] + for fc_edge in fragments.Edges: + edge = [find_vertex(v) for v in fc_edge.Vertexes] + if len(edge) == 2: + edges.append(edge) + + faces = [] + for fc_face in fragments.Faces: + incident_verts = defaultdict(set) + for fc_edge in fc_face.Edges: + edge = [find_vertex(v) for v in fc_edge.Vertexes] + if len(edge) == 2: + i, j = edge + incident_verts[i].add(j) + incident_verts[j].add(i) + + face = [find_vertex(v) for v in fc_face.Vertexes] + + vert_idx = face[0] + correct_face = [vert_idx] + + for i in range(len(face)): + incident = list(incident_verts[vert_idx]) + other_verts = [i for i in incident if i not in correct_face] + if not other_verts: + break + other_vert_idx = other_verts[0] + correct_face.append(other_vert_idx) + vert_idx = other_vert_idx + + if len(correct_face) > 2: + faces.append(correct_face) + + return verts, edges, faces + +class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Voronoi Solid + Tooltip: Generate Voronoi diagram on the Solid object + """ + bl_idname = 'SvVoronoiOnSolidNode' + bl_label = 'Voronoi on Solid' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VORONOI' + + modes = [ + ('FACES', "Surface - Inner", "Generate inner regions of Voronoi diagram on the surface of the solid", 0), + ('WIRE', "Surface - Outer", "Cut inner regions of Voronoi diagram from solid surface", 1), + ('REGIONS', "Volume", "Split volume of the solid body into regions of Voronoi diagram", 2), + ('NEGVOLUME', "Negative Volume", "Cut regions of Voronoi diagram from the volume of the solid object", 3), + ('MESH', "Mesh Faces", "Generate mesh", 4) + ] + + @throttle_and_update_node + def update_sockets(self, context): + self.outputs['Vertices'].hide_safe = self.mode != 'MESH' + self.outputs['Edges'].hide_safe = self.mode != 'MESH' + self.outputs['Faces'].hide_safe = self.mode != 'MESH' + self.outputs['Solids'].hide_safe = self.mode not in {'FACES', 'WIRE', 'REGIONS', 'NEGVOLUME'} + + mode : EnumProperty( + name = "Mode", + items = modes, + update = update_sockets) + + accuracy : IntProperty( + name = "Accuracy", + description = "Accuracy for mesh to solid transformation", + default = 6, + min = 1, + update = updateNode) + + inset : FloatProperty( + name = "Inset", + min = 0.0, max = 1.0, + default = 0.0, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvSolidSocket', 'Solid') + self.inputs.new('SvVerticesSocket', "Sites") + self.inputs.new('SvStringsSocket', "Inset").prop_name = 'inset' + self.outputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvStringsSocket', "Edges") + self.outputs.new('SvStringsSocket', "Faces") + self.outputs.new('SvSolidSocket', "Solids") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode") + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'accuracy') + + def process(self): + + if not any(socket.is_linked for socket in self.outputs): + return + + solid_in = self.inputs['Solid'].sv_get() + sites_in = self.inputs['Sites'].sv_get() + inset_in = self.inputs['Inset'].sv_get() + + solid_in = ensure_nesting_level(solid_in, 2, data_types=(Part.Shape,)) + input_level = get_data_nesting_level(sites_in) + sites_in = ensure_nesting_level(sites_in, 4) + inset_in = ensure_nesting_level(inset_in, 2) + + nested_output = input_level > 3 + + precision = 10 ** (-self.accuracy) + + verts_out = [] + edges_out = [] + faces_out = [] + fragment_faces_out = [] + for params in zip_long_repeat(solid_in, sites_in, inset_in): + new_verts = [] + new_edges = [] + new_faces = [] + new_fragment_faces = [] + for solid, sites, inset in zip_long_repeat(*params): + verts, edges, faces = voronoi_on_solid(solid, sites, + do_clip=True, clipping=None) + + if inset != 0.0: + scale = 1.0 - inset + verts = [scale_relative(vs, site, scale) for vs, site in zip(verts, sites)] + + fragments = [svmesh_to_solid(vs, fs, precision) for vs, fs in zip(verts, faces)] + + if solid.Shells: + shell = solid.Shells[0] + else: + shell = Part.Shell(solid.Faces) + + if self.mode == 'FACES': + fragments = [shell.common(fragment) for fragment in fragments] + elif self.mode == 'WIRE': + fragments = [shell.cut(fragments)] + elif self.mode == 'REGIONS': + fragments = [solid.common(fragment) for fragment in fragments] + elif self.mode == 'NEGVOLUME': + fragments = [solid.cut(fragments)] + else: # MESH + fragments = shell.common(fragments) + + if self.mode in {'FACES', 'WIRE', 'REGIONS', 'NEGVOLUME'}: + new_fragment_faces.append(fragments) + else: # MESH + verts, edges, faces = mesh_from_faces(fragments) + + new_verts.append(verts) + new_edges.append(edges) + new_faces.append(faces) + + if nested_output: + verts_out.append(new_verts) + edges_out.append(new_edges) + faces_out.append(new_faces) + fragment_faces_out.append(new_fragment_faces) + else: + verts_out.extend(new_verts) + edges_out.extend(new_edges) + faces_out.extend(new_faces) + fragment_faces_out.extend(new_fragment_faces) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Edges'].sv_set(edges_out) + self.outputs['Faces'].sv_set(faces_out) + self.outputs['Solids'].sv_set(fragment_faces_out) + +def register(): + if scipy is not None and FreeCAD is not None: + bpy.utils.register_class(SvVoronoiOnSolidNode) + +def unregister(): + if scipy is not None and FreeCAD is not None: + bpy.utils.unregister_class(SvVoronoiOnSolidNode) + -- GitLab From bd7dd051848d8647245cd9291b8b3f08b9fac505 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 13 Dec 2020 15:14:59 +0500 Subject: [PATCH 14/39] Minor update. --- utils/field/probe.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/utils/field/probe.py b/utils/field/probe.py index e2e2d3425..7ff507e2b 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -8,20 +8,18 @@ import random import numpy as np -from mathutils.kdtree import KDTree - from sverchok.utils.field.scalar import SvScalarField from sverchok.utils.logging import error +from sverchok.utils.kdtree import SvKdTree BATCH_SIZE = 50 MAX_ITERATIONS = 1000 def _check_min_distance(v_new, vs_old, min_r): - kdt = KDTree(len(vs_old)) - for i, v in enumerate(vs_old): - kdt.insert(v, i) - kdt.balance() - nearest, idx, dist = kdt.find(v_new) + if not vs_old: + return True + kdt = SvKdTree.new(SvKdTree.BLENDER, vs_old) + nearest, idx, dist = kdt.query(v_new) if dist is None: return True return (dist >= min_r) -- GitLab From 73ad037531db4259717e2babcdd352cb564d8ef3 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 13 Dec 2020 15:37:19 +0500 Subject: [PATCH 15/39] Remopve unused parameter --- nodes/spatial/populate_solid.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/nodes/spatial/populate_solid.py b/nodes/spatial/populate_solid.py index b6969b9c1..0fbbd4799 100644 --- a/nodes/spatial/populate_solid.py +++ b/nodes/spatial/populate_solid.py @@ -121,11 +121,11 @@ class SvPopulateSolidMk2Node(bpy.types.Node, SverchCustomTreeNode): default = False, update = updateNode) - flat_output : BoolProperty( - name = "Flat output", - description = "If checked, generate one flat list of vertices for all input solids; otherwise, generate a separate list of vertices for each solid", - default = False, - update = updateNode) +# flat_output : BoolProperty( +# name = "Flat output", +# description = "If checked, generate one flat list of vertices for all input solids; otherwise, generate a separate list of vertices for each solid", +# default = False, +# update = updateNode) def draw_buttons(self, context, layout): layout.prop(self, "gen_mode", text='Mode') @@ -135,7 +135,7 @@ class SvPopulateSolidMk2Node(bpy.types.Node, SverchCustomTreeNode): layout.prop(self, "in_surface") if self.distance_mode == 'FIELD': layout.prop(self, 'random_radius') - layout.prop(self, "flat_output") +# layout.prop(self, "flat_output") def draw_buttons_ext(self, context, layout): self.draw_buttons(context, layout) @@ -272,12 +272,8 @@ class SvPopulateSolidMk2Node(bpy.types.Node, SverchCustomTreeNode): else: verts, radiuses = self.generate_surface(solid, field, count, min_r, radius, threshold, field_min, field_max, mask) - if self.flat_output: - new_verts.extend(verts) - new_radius.extend(radiuses) - else: - new_verts.append(verts) - new_radius.append(radiuses) + new_verts.append(verts) + new_radius.append(radiuses) if nested_output: verts_out.append(new_verts) -- GitLab From d5852d2827c0616022c4b1c4e3232bac15633c03 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 13 Dec 2020 15:37:24 +0500 Subject: [PATCH 16/39] Do not connect to hidden sockets. --- ui/nodeview_rclick_menu.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/nodeview_rclick_menu.py b/ui/nodeview_rclick_menu.py index ac6b90888..e1dcf521b 100644 --- a/ui/nodeview_rclick_menu.py +++ b/ui/nodeview_rclick_menu.py @@ -66,6 +66,9 @@ def get_output_sockets_map(node): # we can surely use regex for this, but for now this will work. for socket in node.outputs: + if socket.hide or socket.hide_safe: + continue + socket_name = socket.name.lower() if not got_verts and ('ver' in socket_name or 'vec' in socket_name): -- GitLab From c0505ab2af1f56c7ca487c360c5c4b0e0da45203 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 13 Dec 2020 16:47:43 +0500 Subject: [PATCH 17/39] Voronoi on solid: rework modes. --- nodes/spatial/voronoi_on_solid.py | 99 +++++++++++-------------------- 1 file changed, 35 insertions(+), 64 deletions(-) diff --git a/nodes/spatial/voronoi_on_solid.py b/nodes/spatial/voronoi_on_solid.py index 2413caed1..cd9c5e70d 100644 --- a/nodes/spatial/voronoi_on_solid.py +++ b/nodes/spatial/voronoi_on_solid.py @@ -29,7 +29,7 @@ from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_up from sverchok.utils.sv_bmesh_utils import recalc_normals from sverchok.utils.voronoi3d import voronoi_on_solid from sverchok.utils.geom import scale_relative -from sverchok.utils.solid import svmesh_to_solid, SvSolidTopology +from sverchok.utils.solid import svmesh_to_solid, SvSolidTopology, SvGeneralFuse from sverchok.utils.surface.freecad import SvSolidFaceSurface from sverchok.utils.dummy_nodes import add_dummy from sverchok.dependencies import scipy, FreeCAD @@ -97,24 +97,14 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): sv_icon = 'SV_VORONOI' modes = [ - ('FACES', "Surface - Inner", "Generate inner regions of Voronoi diagram on the surface of the solid", 0), - ('WIRE', "Surface - Outer", "Cut inner regions of Voronoi diagram from solid surface", 1), - ('REGIONS', "Volume", "Split volume of the solid body into regions of Voronoi diagram", 2), - ('NEGVOLUME', "Negative Volume", "Cut regions of Voronoi diagram from the volume of the solid object", 3), - ('MESH', "Mesh Faces", "Generate mesh", 4) + ('SURFACE', "Surface", "Generate regions of Voronoi diagram on the surface of the solid", 0), + ('VOLUME', "Volume", "Split volume of the solid body into regions of Voronoi diagram", 2) ] - @throttle_and_update_node - def update_sockets(self, context): - self.outputs['Vertices'].hide_safe = self.mode != 'MESH' - self.outputs['Edges'].hide_safe = self.mode != 'MESH' - self.outputs['Faces'].hide_safe = self.mode != 'MESH' - self.outputs['Solids'].hide_safe = self.mode not in {'FACES', 'WIRE', 'REGIONS', 'NEGVOLUME'} - mode : EnumProperty( name = "Mode", items = modes, - update = update_sockets) + update = updateNode) accuracy : IntProperty( name = "Accuracy", @@ -133,11 +123,8 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvSolidSocket', 'Solid') self.inputs.new('SvVerticesSocket', "Sites") self.inputs.new('SvStringsSocket', "Inset").prop_name = 'inset' - self.outputs.new('SvVerticesSocket', "Vertices") - self.outputs.new('SvStringsSocket', "Edges") - self.outputs.new('SvStringsSocket', "Faces") - self.outputs.new('SvSolidSocket', "Solids") - self.update_sockets(context) + self.outputs.new('SvSolidSocket', "InnerSolid") + self.outputs.new('SvSolidSocket', "OuterSolid") def draw_buttons(self, context, layout): layout.prop(self, "mode") @@ -161,18 +148,16 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): inset_in = ensure_nesting_level(inset_in, 2) nested_output = input_level > 3 + need_inner = self.outputs['InnerSolid'].is_linked + need_outer = self.outputs['OuterSolid'].is_linked precision = 10 ** (-self.accuracy) - verts_out = [] - edges_out = [] - faces_out = [] - fragment_faces_out = [] + inner_fragments_out = [] + outer_fragments_out = [] for params in zip_long_repeat(solid_in, sites_in, inset_in): - new_verts = [] - new_edges = [] - new_faces = [] - new_fragment_faces = [] + new_inner_fragments = [] + new_outer_fragments = [] for solid, sites, inset in zip_long_repeat(*params): verts, edges, faces = voronoi_on_solid(solid, sites, do_clip=True, clipping=None) @@ -183,46 +168,32 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): fragments = [svmesh_to_solid(vs, fs, precision) for vs, fs in zip(verts, faces)] - if solid.Shells: - shell = solid.Shells[0] - else: - shell = Part.Shell(solid.Faces) - - if self.mode == 'FACES': - fragments = [shell.common(fragment) for fragment in fragments] - elif self.mode == 'WIRE': - fragments = [shell.cut(fragments)] - elif self.mode == 'REGIONS': - fragments = [solid.common(fragment) for fragment in fragments] - elif self.mode == 'NEGVOLUME': - fragments = [solid.cut(fragments)] - else: # MESH - fragments = shell.common(fragments) - - if self.mode in {'FACES', 'WIRE', 'REGIONS', 'NEGVOLUME'}: - new_fragment_faces.append(fragments) - else: # MESH - verts, edges, faces = mesh_from_faces(fragments) - - new_verts.append(verts) - new_edges.append(edges) - new_faces.append(faces) + if self.mode == 'SURFACE': + if solid.Shells: + shell = solid.Shells[0] + else: + shell = Part.Shell(solid.Faces) + src = shell + else: # VOLUME + src = solid + + if need_inner: + inner = [src.common(fragment) for fragment in fragments] + new_inner_fragments.append(inner) + + if need_outer: + outer = [src.cut(fragments)] + new_outer_fragments.append(outer) if nested_output: - verts_out.append(new_verts) - edges_out.append(new_edges) - faces_out.append(new_faces) - fragment_faces_out.append(new_fragment_faces) + inner_fragments_out.append(new_inner_fragments) + outer_fragments_out.append(new_outer_fragments) else: - verts_out.extend(new_verts) - edges_out.extend(new_edges) - faces_out.extend(new_faces) - fragment_faces_out.extend(new_fragment_faces) - - self.outputs['Vertices'].sv_set(verts_out) - self.outputs['Edges'].sv_set(edges_out) - self.outputs['Faces'].sv_set(faces_out) - self.outputs['Solids'].sv_set(fragment_faces_out) + inner_fragments_out.extend(new_inner_fragments) + outer_fragments_out.extend(new_outer_fragments) + + self.outputs['InnerSolid'].sv_set(inner_fragments_out) + self.outputs['OuterSolid'].sv_set(outer_fragments_out) def register(): if scipy is not None and FreeCAD is not None: -- GitLab From 213739dff80322e772f43bded08f0467321e5e6e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 13 Dec 2020 20:15:46 +0500 Subject: [PATCH 18/39] Minor fixes. --- nodes/spatial/voronoi_on_mesh.py | 37 +++++++++++++------------------- utils/geom.py | 2 ++ utils/voronoi3d.py | 18 ++++++++++++---- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py index 789c76618..4d8a68b42 100644 --- a/nodes/spatial/voronoi_on_mesh.py +++ b/nodes/spatial/voronoi_on_mesh.py @@ -51,12 +51,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): #('REGIONS', "Regions near Surface", "Generate regions of 3D Voronoi diagram near the surface of the mesh", 3) ] -# thickness : FloatProperty( -# name = "Thickness", -# default = 1.0, -# min = 0.0, -# update=updateNode) - spacing : FloatProperty( name = "Spacing", default = 0.0, @@ -70,7 +64,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): @throttle_and_update_node def update_sockets(self, context): - #self.inputs['Thickness'].hide_safe = self.mode not in {'RIDGES', 'REGIONS'} self.inputs['Spacing'].hide_safe = self.mode not in {'VOLUME', 'SURFACE'} mode : EnumProperty( @@ -79,16 +72,6 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): default = 'VOLUME', update = update_sockets) -# clip_inner : BoolProperty( -# name = "Clip Inner", -# default = True, -# update = updateNode) -# -# clip_outer : BoolProperty( -# name = "Clip Outer", -# default = True, -# update = updateNode) - join_modes = [ ('FLAT', "Flat list", "Output a single flat list of mesh objects (Voronoi diagram ridges / regions) for all input meshes", 0), ('SEPARATE', "Separate lists", "Output a separate list of mesh objects (Voronoi diagram ridges / regions) for each input mesh", 1), @@ -101,6 +84,13 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): default = 'FLAT', update = updateNode) + accuracy : IntProperty( + name = "Accuracy", + description = "Accuracy for mesh bisecting procedure", + default = 6, + min = 1, + update = updateNode) + def sv_init(self, context): self.inputs.new('SvVerticesSocket', 'Vertices') self.inputs.new('SvStringsSocket', 'Faces') @@ -115,14 +105,14 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons(self, context, layout): layout.label(text="Mode:") layout.prop(self, "mode", text='') -# if self.mode in {'REGIONS', 'RIDGES'}: -# row = layout.row(align=True) -# row.prop(self, 'clip_inner', toggle=True) -# row.prop(self, 'clip_outer', toggle=True) layout.prop(self, 'normals') layout.label(text='Output nesting:') layout.prop(self, 'join_mode', text='') + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'accuracy') + def process(self): if not any(socket.is_linked for socket in self.outputs): @@ -143,6 +133,8 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): nested_output = input_level > 3 + precision = 10 ** (-self.accuracy) + verts_out = [] edges_out = [] faces_out = [] @@ -155,7 +147,8 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): spacing = spacing, #clip_inner = self.clip_inner, clip_outer = self.clip_outer, do_clip=True, clipping=None, - mode = self.mode) + mode = self.mode, + precision = precision) if self.normals: verts, edges, faces = recalc_normals(verts, edges, faces, loop=True) diff --git a/utils/geom.py b/utils/geom.py index c4e4a9081..8a869b405 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -725,6 +725,8 @@ class PlaneEquation(object): @classmethod def from_normal_and_point(cls, normal, point): a, b, c = tuple(normal) + if (a*a + b*b + c*c) < 1e-8: + raise Exception("Plane normal is (almost) zero!") cx, cy, cz = tuple(point) d = - (a*cx + b*cy + c*cz) return PlaneEquation(a, b, c, d) diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index e12e8797c..c6e184b82 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -231,7 +231,7 @@ def calc_bvh_projections(bvh, sites): projections.append(loc) return np.array(projections) -def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True): +def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True, precision=1e-8): def get_ridges_per_site(voronoi): result = defaultdict(list) @@ -249,6 +249,8 @@ def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True): def cut_cell(verts, faces, planes, site): src_mesh = bmesh_from_pydata(verts, [], faces, normal_update=True) for plane in planes: + if len(src_mesh.verts) == 0: + break geom_in = src_mesh.verts[:] + src_mesh.edges[:] + src_mesh.faces[:] plane_co = plane.projection_of_point(site) @@ -257,9 +259,15 @@ def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True): plane_no = - plane_no plane_co = plane_co - 0.5 * spacing * plane_no.normalized() + + current_verts = np.array([tuple(v.co) for v in src_mesh.verts]) + signs = PlaneEquation.from_normal_and_point(plane_no, plane_co).side_of_points(current_verts) + if (signs < 0).all(): # or (signs < 0).all(): + continue + #print(f"Plane co {plane_co}, no {plane_no}") res = bmesh.ops.bisect_plane( - src_mesh, geom=geom_in, dist=0.00001, + src_mesh, geom=geom_in, dist=precision, plane_co = plane_co, plane_no = plane_no, use_snap_center = False, @@ -292,7 +300,8 @@ def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True): 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'): + clipping=1.0, mode = 'REGIONS', + precision = 1e-8): bvh = BVHTree.FromPolygons(verts, faces) npoints = len(sites) @@ -325,7 +334,8 @@ def voronoi_on_mesh(verts, faces, sites, thickness, plus_points = sites + clipping*normals all_points.extend(plus_points.tolist()) return voronoi_on_mesh_bmesh(verts, faces, all_points, - spacing = spacing, fill = (mode == 'VOLUME')) + spacing = spacing, fill = (mode == 'VOLUME'), + precision = precision) def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True, predicate_plus=None, predicate_minus=None): k = 0.5*thickness -- GitLab From cd65945e47dbd359989d79a4ac9e8e7d8938aa0b Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 14 Dec 2020 21:21:03 +0500 Subject: [PATCH 19/39] Remove excessive parameter. --- nodes/spatial/populate_surface.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/nodes/spatial/populate_surface.py b/nodes/spatial/populate_surface.py index ace1d9fef..8ccff1026 100644 --- a/nodes/spatial/populate_surface.py +++ b/nodes/spatial/populate_surface.py @@ -89,18 +89,11 @@ class SvPopulateSurfaceMk2Node(bpy.types.Node, SverchCustomTreeNode): default = False, update = updateNode) - flat_output : BoolProperty( - name = "Flat output", - description = "If checked, generate one flat list of vertices for all input surfaces; otherwise, generate a separate list of vertices for each surface", - default = False, - update = updateNode) - def draw_buttons(self, context, layout): layout.prop(self, 'distance_mode') layout.prop(self, "proportional") if self.distance_mode == 'FIELD': layout.prop(self, 'random_radius') - layout.prop(self, "flat_output") def sv_init(self, context): self.inputs.new('SvSurfaceSocket', "Surface") @@ -174,14 +167,9 @@ class SvPopulateSurfaceMk2Node(bpy.types.Node, SverchCustomTreeNode): min_r = min_r, min_r_field = radius, random_radius = self.random_radius, seed = seed) - if self.flat_output: - new_verts.extend(verts) - new_uv.extend(uvs) - new_radius.extend(radiuses) - else: - new_verts.append(verts) - new_uv.append(uvs) - new_radius.append(radiuses) + new_verts.append(verts) + new_uv.append(uvs) + new_radius.append(radiuses) if nested_output: verts_out.append(new_verts) -- GitLab From 0449894de1605b8b0c739fb926015f169a3a42aa Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 14 Dec 2020 23:40:13 +0500 Subject: [PATCH 20/39] "voronoi on solid": "flat output" parameter. --- nodes/spatial/voronoi_on_solid.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/nodes/spatial/voronoi_on_solid.py b/nodes/spatial/voronoi_on_solid.py index cd9c5e70d..0cc0b9353 100644 --- a/nodes/spatial/voronoi_on_solid.py +++ b/nodes/spatial/voronoi_on_solid.py @@ -119,6 +119,12 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): default = 0.0, update = updateNode) + flat_output : BoolProperty( + name = "Flat output", + description = "If checked, output single flat list of fragments for all input solids; otherwise, output a separate list of fragments for each solid.", + default = True, + update = updateNode) + def sv_init(self, context): self.inputs.new('SvSolidSocket', 'Solid') self.inputs.new('SvVerticesSocket', "Sites") @@ -128,6 +134,7 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons(self, context, layout): layout.prop(self, "mode") + layout.prop(self, "flat_output") def draw_buttons_ext(self, context, layout): self.draw_buttons(context, layout) @@ -179,11 +186,17 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): if need_inner: inner = [src.common(fragment) for fragment in fragments] - new_inner_fragments.append(inner) + if self.flat_output: + new_inner_fragments.extend(inner) + else: + new_inner_fragments.append(inner) if need_outer: outer = [src.cut(fragments)] - new_outer_fragments.append(outer) + if self.flat_output: + new_outer_fragments.extend(outer) + else: + new_outer_fragments.append(outer) if nested_output: inner_fragments_out.append(new_inner_fragments) -- GitLab From b2cb5a72a88b1e05d1a4fd185607d43ec7579e7b Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 19 Dec 2020 22:48:21 +0500 Subject: [PATCH 21/39] "Solid to mesh": "trivial" mode. --- nodes/solid/solid_to_mesh_mk2.py | 33 ++++++++++++++++++++--- utils/solid.py | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/nodes/solid/solid_to_mesh_mk2.py b/nodes/solid/solid_to_mesh_mk2.py index 77c9b5815..9cbf5fa55 100644 --- a/nodes/solid/solid_to_mesh_mk2.py +++ b/nodes/solid/solid_to_mesh_mk2.py @@ -12,6 +12,8 @@ else: from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, match_long_repeat as mlr + from sverchok.utils.solid import mesh_from_solid_faces + from sverchok.utils.sv_bmesh_utils import recalc_normals import MeshPart @@ -31,6 +33,7 @@ else: ('Standard', 'Standard', '', 1), ('Mefisto', 'Mefisto', '', 2), # ('NetGen', 'NetGen', '', 3), + ('Trivial', 'Trivial', '', 10) ] shape_types = [ ('Solid', 'Solid', '', 0), @@ -66,9 +69,12 @@ else: self.inputs['Surface Deviation'].hide_safe = True self.inputs['Angle Deviation'].hide_safe = True self.inputs['Max Edge Length'].hide_safe = False - - - + + elif self.mode == 'Trivial': + self.inputs['Precision'].hide_safe = True + self.inputs['Surface Deviation'].hide_safe = True + self.inputs['Angle Deviation'].hide_safe = True + self.inputs['Max Edge Length'].hide_safe = True precision: FloatProperty( name="Precision", @@ -197,6 +203,23 @@ else: return verts, faces + def trivial_mesher(self): + solids = self.inputs[self["shape_type"]].sv_get() + + verts = [] + faces = [] + for solid in solids: + if self.shape_type == 'Solid': + shape = solid + else: + shape = solid.face + new_verts, new_edges, new_faces = mesh_from_solid_faces(shape) + new_verts, new_edges, new_faces = recalc_normals(new_verts, new_edges, new_faces) + + verts.append(new_verts) + faces.append(new_faces) + + return verts, faces def process(self): if not any(socket.is_linked for socket in self.outputs): @@ -206,8 +229,10 @@ else: verts, faces = self.basic_mesher() elif self.mode == 'Standard': verts, faces = self.standard_mesher() - else: + elif self.mode == 'Mefisto': verts, faces = self.mefisto_mesher() + else: # Trivial + verts, faces = self.trivial_mesher() self.outputs['Verts'].sv_set(verts) self.outputs['Faces'].sv_set(faces) diff --git a/utils/solid.py b/utils/solid.py index 8a30b5260..e6bae1fbf 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -477,3 +477,49 @@ def svmesh_to_solid(verts, faces, precision, remove_splitter=True): return Part.makeSolid(shape) +def mesh_from_solid_faces(solid): + verts = [(v.X, v.Y, v.Z) for v in solid.Vertexes] + + all_fc_verts = {SvSolidTopology.Item(v) : i for i, v in enumerate(solid.Vertexes)} + def find_vertex(v): + #for i, fc_vert in enumerate(solid.Vertexes): + # if v.isSame(fc_vert): + # return i + #return None + return all_fc_verts[SvSolidTopology.Item(v)] + + edges = [] + for fc_edge in solid.Edges: + edge = [find_vertex(v) for v in fc_edge.Vertexes] + if len(edge) == 2: + edges.append(edge) + + faces = [] + for fc_face in solid.Faces: + incident_verts = defaultdict(set) + for fc_edge in fc_face.Edges: + edge = [find_vertex(v) for v in fc_edge.Vertexes] + if len(edge) == 2: + i, j = edge + incident_verts[i].add(j) + incident_verts[j].add(i) + + face = [find_vertex(v) for v in fc_face.Vertexes] + + vert_idx = face[0] + correct_face = [vert_idx] + + for i in range(len(face)): + incident = list(incident_verts[vert_idx]) + other_verts = [i for i in incident if i not in correct_face] + if not other_verts: + break + other_vert_idx = other_verts[0] + correct_face.append(other_vert_idx) + vert_idx = other_vert_idx + + if len(correct_face) > 2: + faces.append(correct_face) + + return verts, edges, faces + -- GitLab From 91285db70f053f2f41e08e139f9097fe82d40256 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 19 Dec 2020 22:48:45 +0500 Subject: [PATCH 22/39] Remove unused code from the node. --- nodes/spatial/voronoi_on_solid.py | 46 ------------------------------- 1 file changed, 46 deletions(-) diff --git a/nodes/spatial/voronoi_on_solid.py b/nodes/spatial/voronoi_on_solid.py index 0cc0b9353..3bedb4e4e 100644 --- a/nodes/spatial/voronoi_on_solid.py +++ b/nodes/spatial/voronoi_on_solid.py @@ -40,52 +40,6 @@ if scipy is None or FreeCAD is None: if FreeCAD is not None: import Part -def mesh_from_faces(fragments): - verts = [(v.X, v.Y, v.Z) for v in fragments.Vertexes] - - all_fc_verts = {SvSolidTopology.Item(v) : i for i, v in enumerate(fragments.Vertexes)} - def find_vertex(v): - #for i, fc_vert in enumerate(fragments.Vertexes): - # if v.isSame(fc_vert): - # return i - #return None - return all_fc_verts[SvSolidTopology.Item(v)] - - edges = [] - for fc_edge in fragments.Edges: - edge = [find_vertex(v) for v in fc_edge.Vertexes] - if len(edge) == 2: - edges.append(edge) - - faces = [] - for fc_face in fragments.Faces: - incident_verts = defaultdict(set) - for fc_edge in fc_face.Edges: - edge = [find_vertex(v) for v in fc_edge.Vertexes] - if len(edge) == 2: - i, j = edge - incident_verts[i].add(j) - incident_verts[j].add(i) - - face = [find_vertex(v) for v in fc_face.Vertexes] - - vert_idx = face[0] - correct_face = [vert_idx] - - for i in range(len(face)): - incident = list(incident_verts[vert_idx]) - other_verts = [i for i in incident if i not in correct_face] - if not other_verts: - break - other_vert_idx = other_verts[0] - correct_face.append(other_vert_idx) - vert_idx = other_vert_idx - - if len(correct_face) > 2: - faces.append(correct_face) - - return verts, edges, faces - class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): """ Triggers: Voronoi Solid -- GitLab From 22896e8ab9bc4cb17bfee0a0020956a6077fdcad Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 20 Dec 2020 14:09:18 +0500 Subject: [PATCH 23/39] "Lloyd on mesh": volume mode. --- nodes/spatial/lloyd_on_mesh.py | 21 ++++++++++++++-- utils/voronoi3d.py | 46 +++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/nodes/spatial/lloyd_on_mesh.py b/nodes/spatial/lloyd_on_mesh.py index a7cce0f8c..9de9f8f62 100644 --- a/nodes/spatial/lloyd_on_mesh.py +++ b/nodes/spatial/lloyd_on_mesh.py @@ -11,7 +11,7 @@ from bpy.props import FloatProperty, StringProperty, BoolProperty, EnumProperty, from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, ensure_nesting_level, zip_long_repeat, throttle_and_update_node, get_data_nesting_level from sverchok.utils.field.scalar import SvScalarField -from sverchok.utils.voronoi3d import lloyd_on_mesh +from sverchok.utils.voronoi3d import lloyd_on_mesh, lloyd_in_mesh from sverchok.utils.dummy_nodes import add_dummy from sverchok.dependencies import scipy @@ -41,6 +41,20 @@ class SvLloydOnMeshNode(bpy.types.Node, SverchCustomTreeNode): min = 0.0, update=updateNode) + modes = [ + ('SURFACE', "Surface", "Surface", 0), + ('VOLUME', "Volume", "Volume", 1) + ] + + mode : bpy.props.EnumProperty( + name = "Mode", + items = modes, + default = 'SURFACE', + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode") + def sv_init(self, context): self.inputs.new('SvVerticesSocket', "Vertices") self.inputs.new('SvStringsSocket', "Faces") @@ -77,7 +91,10 @@ class SvLloydOnMeshNode(bpy.types.Node, SverchCustomTreeNode): for params in zip_long_repeat(verts_in, faces_in, sites_in, thickness_in, iterations_in, weights_in): new_verts = [] for verts, faces, sites, thickness, iterations, weights in zip_long_repeat(*params): - sites = lloyd_on_mesh(verts, faces, sites, thickness, iterations, weight_field = weights) + if self.mode == 'SURFACE': + sites = lloyd_on_mesh(verts, faces, sites, thickness, iterations, weight_field = weights) + else: + sites = lloyd_in_mesh(verts, faces, sites, iterations, thickness=thickness, weight_field = weights) new_verts.append(sites) if nested_output: verts_out.append(new_verts) diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index c6e184b82..bf9778743 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -25,7 +25,7 @@ import bmesh from mathutils import Vector from mathutils.bvhtree import BVHTree -from sverchok.utils.sv_mesh_utils import mask_vertices, polygons_to_edges +from sverchok.utils.sv_mesh_utils import mask_vertices, polygons_to_edges, point_inside_mesh from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh, bmesh_clip from sverchok.utils.geom import calc_bounds, bounding_sphere, PlaneEquation from sverchok.utils.math import project_to_sphere, weighted_center @@ -403,6 +403,50 @@ def lloyd_on_mesh(verts, faces, sites, thickness, n_iterations, weight_field=Non return points.tolist() +def lloyd_in_mesh(verts, faces, sites, n_iterations, thickness=None, weight_field=None): + bvh = BVHTree.FromPolygons(verts, faces) + + if thickness is None: + x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(verts) + thickness = max(x_max - x_min, y_max - y_min, z_max - z_min) / 4.0 + + def iteration(points): + n = len(points) + + normals = calc_bvh_normals(bvh, points) + k = 0.5*thickness + points = np.array(points) + plus_points = points + k*normals + all_points = points.tolist() + plus_points.tolist() + + diagram = Voronoi(all_points) + centers = [] + for site_idx in range(n): + region_idx = diagram.point_region[site_idx] + region = diagram.regions[region_idx] + region_verts = np.array([diagram.vertices[i] for i in region]) + center = weighted_center(region_verts, weight_field) + centers.append(tuple(center)) + return centers + + def restrict(points): + result = [] + for p in points: + if point_inside_mesh(bvh, p): + result.append(p) + else: + loc, normal, index, distance = bvh.find_nearest(p) + if loc is not None: + result.append(tuple(loc)) + return result + + points = restrict(sites) + for i in range(n_iterations): + points = iteration(points) + points = restrict(points) + + return points + def lloyd_in_solid(solid, sites, n_iterations, tolerance=1e-4, weight_field=None): shell = solid.Shells[0] -- GitLab From de3549a74f5f24929c711efcec6c2133ad896ef8 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 20 Dec 2020 14:09:28 +0500 Subject: [PATCH 24/39] "Random points on mesh": volume mode. --- nodes/spatial/random_points_on_mesh.py | 65 +++++++++++++++++++++----- utils/sv_mesh_utils.py | 18 +++++++ 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/nodes/spatial/random_points_on_mesh.py b/nodes/spatial/random_points_on_mesh.py index 90c526580..c5d0de538 100644 --- a/nodes/spatial/random_points_on_mesh.py +++ b/nodes/spatial/random_points_on_mesh.py @@ -9,14 +9,17 @@ from typing import NamedTuple, Any, List, Tuple import random from itertools import chain, repeat +import numpy as np import bpy from mathutils import Vector +from mathutils.bvhtree import BVHTree from mathutils.geometry import tessellate_polygon, area_tri from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode - +from sverchok.utils.geom import calc_bounds +from sverchok.utils.sv_mesh_utils import point_inside_mesh class SocketProperties(NamedTuple): name: str @@ -46,16 +49,43 @@ INPUT_CONFIG = [ class NodeProperties(NamedTuple): proportional: bool - + mode : str + +MAX_ITERATIONS = 1000 + +def populate_mesh(verts, faces, count, seed): + bvh = BVHTree.FromPolygons(verts, faces) + np.random.seed(seed) + x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(verts) + low = np.array([x_min, y_min, z_min]) + high = np.array([x_max, y_max, z_max]) + result = [] + done = 0 + iterations = 0 + while True: + if iterations > MAX_ITERATIONS: + raise Exception("Iterations limit is reached") + max_pts = max(count, count-done) + points = np.random.uniform(low, high, size=(max_pts,3)).tolist() + points = [p for p in points if point_inside_mesh(bvh, p)] + n = len(points) + result.extend(points) + done += n + iterations += 1 + if done >= count: + break + return result, [] def node_process(inputs: InputData, properties: NodeProperties): - me = TriangulatedMesh([Vector(co) for co in inputs.verts], inputs.faces) - if properties.proportional: - me.use_even_points_distribution() - if inputs.face_weight: - me.set_custom_face_weights(inputs.face_weight) - return me.generate_random_points(inputs.number[0], inputs.seed[0]) # todo [0] <-- ?! - + if properties.mode == 'SURFACE': + me = TriangulatedMesh([Vector(co) for co in inputs.verts], inputs.faces) + if properties.proportional: + me.use_even_points_distribution() + if inputs.face_weight: + me.set_custom_face_weights(inputs.face_weight) + return me.generate_random_points(inputs.number[0], inputs.seed[0]) # todo [0] <-- ?! + else: + return populate_mesh(inputs.verts, inputs.faces, inputs.number[0], inputs.seed[0]) class TriangulatedMesh: def __init__(self, verts: List[Vector], faces: List[List[int]]): @@ -154,9 +184,22 @@ class SvRandomPointsOnMesh(bpy.types.Node, SverchCustomTreeNode): description="If checked, then number of points at each face is proportional to the area of the face", default=True, update=updateNode) + + modes = [ + ('SURFACE', "Surface", "Surface", 0), + ('VOLUME', "Volume", "Volume", 1) + ] + + mode : bpy.props.EnumProperty( + name = "Mode", + items = modes, + default = 'SURFACE', + update=updateNode) def draw_buttons(self, context, layout): - layout.prop(self, "proportional", toggle=True) + layout.prop(self, "mode") + if self.mode == 'SURFACE': + layout.prop(self, "proportional") def sv_init(self, context): [self.inputs.new(p.socket_type, p.name) for p in INPUT_CONFIG] @@ -169,7 +212,7 @@ class SvRandomPointsOnMesh(bpy.types.Node, SverchCustomTreeNode): if not all([self.inputs['Verts'].is_linked, self.inputs['Faces'].is_linked]): return - props = NodeProperties(self.proportional) + props = NodeProperties(self.proportional, self.mode) out = [node_process(inputs, props) for inputs in self.get_input_data_iterator(INPUT_CONFIG)] [s.sv_set(data) for s, data in zip(self.outputs, zip(*out))] diff --git a/utils/sv_mesh_utils.py b/utils/sv_mesh_utils.py index ee3ab554e..094aa0843 100644 --- a/utils/sv_mesh_utils.py +++ b/utils/sv_mesh_utils.py @@ -19,7 +19,9 @@ from sverchok.data_structure import fullList_deep_copy from numpy import array, empty, concatenate, unique, sort, int32, ndarray, vectorize +from mathutils import Vector +from sverchok.data_structure import fullList_deep_copy def mesh_join(vertices_s, edges_s, faces_s): '''Given list of meshes represented by lists of vertices, edges and faces, @@ -142,3 +144,19 @@ def get_unique_faces(faces): else: uniq_faces.append(face) return uniq_faces + +def point_inside_mesh(bvh, point): + point = Vector(point) + axis = Vector((1, 0, 0)) + outside = False + count = 0 + while True: + location, normal, index, distance = bvh.ray_cast(point, axis) + if index is None: + break + count += 1 + point = location + axis * 0.00001 + if count % 2 == 0: + outside = True + return not outside + -- GitLab From 7453dd2b4a06c86ee7e750454b90a7f0b77b75c5 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 20 Dec 2020 18:03:02 +0500 Subject: [PATCH 25/39] "voronoi on mesh" fixes. --- nodes/spatial/voronoi_on_mesh.py | 11 +++++- utils/voronoi3d.py | 61 ++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py index 4d8a68b42..b6dc71625 100644 --- a/nodes/spatial/voronoi_on_mesh.py +++ b/nodes/spatial/voronoi_on_mesh.py @@ -100,6 +100,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): self.outputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvStringsSocket', "Edges") self.outputs.new('SvStringsSocket', "Faces") + self.outputs.new('SvVerticesSocket', "AllSites") self.update_sockets(context) def draw_buttons(self, context, layout): @@ -138,12 +139,14 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): verts_out = [] edges_out = [] faces_out = [] + sites_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 = voronoi_on_mesh(verts, faces, sites, thickness=0, + verts, edges, faces, sites = 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, @@ -156,28 +159,34 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): new_verts.extend(verts) new_edges.extend(edges) new_faces.extend(faces) + new_sites.extend(sites) elif self.join_mode == 'SEPARATE': new_verts.append(verts) new_edges.append(edges) new_faces.append(faces) + new_sites.append(sites) 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(sites) if nested_output: verts_out.append(new_verts) edges_out.append(new_edges) faces_out.append(new_faces) + sites_out.append(new_sites) else: verts_out.extend(new_verts) edges_out.extend(new_edges) faces_out.extend(new_faces) + sites_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['AllSites'].sv_set(sites_out) def register(): if scipy is not None: diff --git a/utils/voronoi3d.py b/utils/voronoi3d.py index bf9778743..cad782fd9 100644 --- a/utils/voronoi3d.py +++ b/utils/voronoi3d.py @@ -220,7 +220,7 @@ def calc_bvh_normals(bvh, sites): for site in sites: loc, normal, index, distance = bvh.find_nearest(site) if loc is not None: - normals.append(normal) + normals.append(normal.normalized()) return np.array(normals) def calc_bvh_projections(bvh, sites): @@ -231,7 +231,7 @@ def calc_bvh_projections(bvh, sites): projections.append(loc) return np.array(projections) -def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True, precision=1e-8): +def voronoi_on_mesh_bmesh(verts, faces, n_orig_sites, sites, spacing=0.0, fill=True, precision=1e-8): def get_ridges_per_site(voronoi): result = defaultdict(list) @@ -248,24 +248,25 @@ def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True, precision def cut_cell(verts, faces, planes, site): src_mesh = bmesh_from_pydata(verts, [], faces, normal_update=True) + n_cuts = 0 for plane in planes: if len(src_mesh.verts) == 0: break geom_in = src_mesh.verts[:] + src_mesh.edges[:] + src_mesh.faces[:] plane_co = plane.projection_of_point(site) - plane_no = plane.normal + plane_no = plane.normal.normalized() if plane.side_of_point(site) > 0: plane_no = - plane_no - plane_co = plane_co - 0.5 * spacing * plane_no.normalized() + plane_co = plane_co - 0.5 * spacing * plane_no current_verts = np.array([tuple(v.co) for v in src_mesh.verts]) signs = PlaneEquation.from_normal_and_point(plane_no, plane_co).side_of_points(current_verts) - if (signs < 0).all(): # or (signs < 0).all(): + #print(f"Plane co {plane_co}, no {plane_no}, signs {signs}") + if (signs <= 0).all():# or (signs <= 0).all(): continue - #print(f"Plane co {plane_co}, no {plane_no}") res = bmesh.ops.bisect_plane( src_mesh, geom=geom_in, dist=precision, plane_co = plane_co, @@ -274,11 +275,17 @@ def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True, precision clear_outer = True, clear_inner = False ) + n_cuts += 1 if fill: surround = [e for e in res['geom_cut'] if isinstance(e, bmesh.types.BMEdge)] - fres = bmesh.ops.edgenet_prepare(src_mesh, edges=surround) - bmesh.ops.edgeloop_fill(src_mesh, edges=fres['edges']) + if surround: + fres = bmesh.ops.edgenet_prepare(src_mesh, edges=surround) + if fres['edges']: + bmesh.ops.edgeloop_fill(src_mesh, edges=fres['edges']) + + if n_cuts == 0: + return None return pydata_from_bmesh(src_mesh) @@ -289,11 +296,13 @@ def voronoi_on_mesh_bmesh(verts, faces, sites, spacing=0.0, fill=True, precision voronoi = Voronoi(np.array(sites)) ridges_per_site = get_ridges_per_site(voronoi) for site_idx in range(len(sites)): - new_verts, new_edges, new_faces = cut_cell(verts, faces, ridges_per_site[site_idx], sites[site_idx]) - if new_verts: - verts_out.append(new_verts) - edges_out.append(new_edges) - faces_out.append(new_faces) + cell = cut_cell(verts, faces, ridges_per_site[site_idx], sites[site_idx]) + 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) return verts_out, edges_out, faces_out @@ -328,14 +337,17 @@ def voronoi_on_mesh(verts, faces, sites, thickness, clipping = clipping) else: # VOLUME, SURFACE - all_points = sites + all_points = sites[:] if do_clip: - normals = calc_bvh_normals(bvh, sites) - plus_points = sites + clipping*normals - all_points.extend(plus_points.tolist()) - return voronoi_on_mesh_bmesh(verts, faces, all_points, + for site in sites: + loc, normal, index, distance = bvh.find_nearest(site) + if loc is not None: + p1 = loc + clipping * normal + all_points.append(p1) + verts, edges, faces = voronoi_on_mesh_bmesh(verts, faces, len(sites), all_points, spacing = spacing, fill = (mode == 'VOLUME'), precision = precision) + return verts, edges, faces, all_points def project_solid_normals(shell, pts, thickness, add_plus=True, add_minus=True, predicate_plus=None, predicate_minus=None): k = 0.5*thickness @@ -410,14 +422,19 @@ def lloyd_in_mesh(verts, faces, sites, n_iterations, thickness=None, weight_fiel x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(verts) thickness = max(x_max - x_min, y_max - y_min, z_max - z_min) / 4.0 + epsilon = 1e-8 + def iteration(points): n = len(points) - normals = calc_bvh_normals(bvh, points) + all_points = points[:] k = 0.5*thickness - points = np.array(points) - plus_points = points + k*normals - all_points = points.tolist() + plus_points.tolist() + for p in points: + p = Vector(p) + loc, normal, index, distance = bvh.find_nearest(p) + if distance <= epsilon: + p1 = p + k * normal + all_points.append(tuple(p1)) diagram = Voronoi(all_points) centers = [] -- GitLab From 3d28cf5d897858e09fe7434ecc49d3983ccdc9fd Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 20 Dec 2020 18:13:34 +0500 Subject: [PATCH 26/39] Bugfix. --- utils/field/probe.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/utils/field/probe.py b/utils/field/probe.py index 7ff507e2b..9ed5805a3 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -22,7 +22,10 @@ def _check_min_distance(v_new, vs_old, min_r): nearest, idx, dist = kdt.query(v_new) if dist is None: return True - return (dist >= min_r) + ok = (dist >= min_r) + if not ok: + print(f"V {v_new} => {nearest}, {dist} >= {min_r}") + return ok def _check_min_radius(point, old_points, old_radiuses, min_r): if not old_points: @@ -136,6 +139,7 @@ def field_random_probe(field, bbox, count, for candidate in candidates: if _check_min_distance(candidate, generated_verts + good_verts, min_r): good_verts.append(candidate) + good_radiuses = [1 for c in good_verts] if predicate is not None: pairs = [(vert, r) for vert, r in zip(good_verts, good_radiuses) if predicate(vert)] -- GitLab From d02ef06c0499f3691cf851423a8d60809bc21be5 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Dec 2020 01:21:54 +0500 Subject: [PATCH 27/39] Optimization for "voronoi on solid". --- nodes/solid/polygon_face.py | 6 ++--- nodes/spatial/voronoi_on_solid.py | 2 +- utils/solid.py | 43 ++++++++++++++++++++++--------- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/nodes/solid/polygon_face.py b/nodes/solid/polygon_face.py index 7ff202a36..20ae40372 100644 --- a/nodes/solid/polygon_face.py +++ b/nodes/solid/polygon_face.py @@ -43,9 +43,9 @@ class SvSolidPolygonFaceNode(bpy.types.Node, SverchCustomTreeNode): for face_i in face_idxs: face_i = list(face_i) face_i.append(face_i[0]) - verts = [verts[idx] for idx in face_i] - verts = [Base.Vector(*vert) for vert in verts] - wire = Part.makePolygon(verts) + fc_verts = [verts[idx] for idx in face_i] + fc_verts = [Base.Vector(*vert) for vert in fc_verts] + wire = Part.makePolygon(fc_verts) face = Part.Face(wire) surface = SvSolidFaceSurface(face)#.to_nurbs() result.append(surface) diff --git a/nodes/spatial/voronoi_on_solid.py b/nodes/spatial/voronoi_on_solid.py index 3bedb4e4e..828cf5b3f 100644 --- a/nodes/spatial/voronoi_on_solid.py +++ b/nodes/spatial/voronoi_on_solid.py @@ -29,7 +29,7 @@ from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_up from sverchok.utils.sv_bmesh_utils import recalc_normals from sverchok.utils.voronoi3d import voronoi_on_solid from sverchok.utils.geom import scale_relative -from sverchok.utils.solid import svmesh_to_solid, SvSolidTopology, SvGeneralFuse +from sverchok.utils.solid import BMESH, svmesh_to_solid, SvSolidTopology, SvGeneralFuse from sverchok.utils.surface.freecad import SvSolidFaceSurface from sverchok.utils.dummy_nodes import add_dummy from sverchok.dependencies import scipy, FreeCAD diff --git a/utils/solid.py b/utils/solid.py index e6bae1fbf..fd515171e 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -454,7 +454,10 @@ def mefisto_mesher(solids, max_edge_length): return verts, faces -def svmesh_to_solid(verts, faces, precision, remove_splitter=True): +FCMESH = 'FCMESH' +BMESH = 'BMESH' + +def svmesh_to_solid(verts, faces, precision=1e-6, remove_splitter=True, method=FCMESH): """ input: verts: list of 3element iterables, [vector, vector...] @@ -465,17 +468,33 @@ def svmesh_to_solid(verts, faces, precision, remove_splitter=True): a FreeCAD solid """ - tri_faces = ensure_triangles(verts, faces, True) - faces_t = [[verts[c] for c in f] for f in tri_faces] - mesh = Mesh.Mesh(faces_t) - shape = Part.Shape() - shape.makeShapeFromMesh(mesh.Topology, precision) - - if remove_splitter: - # may slow it down, or be totally necessary - shape = shape.removeSplitter() - - return Part.makeSolid(shape) + if method == FCMESH: + tri_faces = ensure_triangles(verts, faces, True) + faces_t = [[verts[c] for c in f] for f in tri_faces] + mesh = Mesh.Mesh(faces_t) + shape = Part.Shape() + shape.makeShapeFromMesh(mesh.Topology, precision) + + if remove_splitter: + # may slow it down, or be totally necessary + shape = shape.removeSplitter() + + return Part.makeSolid(shape) + elif method == BMESH: + fc_faces = [] + for face in faces: + face_i = list(face) + [face[0]] + face_verts = [Base.Vector(verts[i]) for i in face_i] + wire = Part.makePolygon(face_verts) + fc_face = Part.Face(wire) + fc_faces.append(fc_face) + shell = Part.makeShell(fc_faces) + solid = Part.makeSolid(shell) + if remove_splitter: + solid = solid.removeSplitter() + return solid + else: + raise Exception("Unsupported method") def mesh_from_solid_faces(solid): verts = [(v.X, v.Y, v.Z) for v in solid.Vertexes] -- GitLab From 24278ef82e8dd2bef796e90bdddaa9b826aa2dce Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Dec 2020 01:23:32 +0500 Subject: [PATCH 28/39] Remove excessive print. --- utils/field/probe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/field/probe.py b/utils/field/probe.py index 9ed5805a3..215fb34f5 100644 --- a/utils/field/probe.py +++ b/utils/field/probe.py @@ -23,8 +23,8 @@ def _check_min_distance(v_new, vs_old, min_r): if dist is None: return True ok = (dist >= min_r) - if not ok: - print(f"V {v_new} => {nearest}, {dist} >= {min_r}") + #if not ok: + # print(f"V {v_new} => {nearest}, {dist} >= {min_r}") return ok def _check_min_radius(point, old_points, old_radiuses, min_r): -- GitLab From eb3364af7288ab03974f8e7abe4a9edb1f1ecb13 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Dec 2020 01:30:39 +0500 Subject: [PATCH 29/39] Faster "voronoi on solid'. --- nodes/spatial/voronoi_on_solid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/spatial/voronoi_on_solid.py b/nodes/spatial/voronoi_on_solid.py index 828cf5b3f..2bf466883 100644 --- a/nodes/spatial/voronoi_on_solid.py +++ b/nodes/spatial/voronoi_on_solid.py @@ -127,7 +127,7 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): scale = 1.0 - inset verts = [scale_relative(vs, site, scale) for vs, site in zip(verts, sites)] - fragments = [svmesh_to_solid(vs, fs, precision) for vs, fs in zip(verts, faces)] + fragments = [svmesh_to_solid(vs, fs, precision, method=BMESH, remove_splitter=False) for vs, fs in zip(verts, faces)] if self.mode == 'SURFACE': if solid.Shells: -- GitLab From ea04a5e0fa936af3f92ac2e7b74ac0cd50b073cf Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Dec 2020 01:48:48 +0500 Subject: [PATCH 30/39] "Polygon face": add "Accuracy" parameter. --- nodes/solid/polygon_face.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nodes/solid/polygon_face.py b/nodes/solid/polygon_face.py index 20ae40372..e54545691 100644 --- a/nodes/solid/polygon_face.py +++ b/nodes/solid/polygon_face.py @@ -8,6 +8,7 @@ import numpy as np import bpy +from bpy.props import BoolProperty, IntProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import zip_long_repeat, ensure_nesting_level, updateNode @@ -38,7 +39,19 @@ class SvSolidPolygonFaceNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvStringsSocket', "Faces") self.outputs.new('SvSurfaceSocket', "SolidFaces") + accuracy : IntProperty( + name = "Accuracy", + description = "Tolerance parameter for checking if ends of edges coincide", + default = 8, + min = 1, + update = updateNode) + + def draw_buttons_ext(self, context, layout): + layout.prop(self, 'accuracy') + def make_faces(self, verts, face_idxs): + tolerance = 10 ** (-self.accuracy) + result = [] for face_i in face_idxs: face_i = list(face_i) @@ -46,6 +59,7 @@ class SvSolidPolygonFaceNode(bpy.types.Node, SverchCustomTreeNode): fc_verts = [verts[idx] for idx in face_i] fc_verts = [Base.Vector(*vert) for vert in fc_verts] wire = Part.makePolygon(fc_verts) + wire.fixTolerance(tolerance) face = Part.Face(wire) surface = SvSolidFaceSurface(face)#.to_nurbs() result.append(surface) -- GitLab From 62095d190fb4e91e6e90d250e49373daa59246b7 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Dec 2020 01:53:37 +0500 Subject: [PATCH 31/39] Precision fix. --- utils/solid.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/utils/solid.py b/utils/solid.py index fd515171e..caf5e1e0f 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -486,7 +486,13 @@ def svmesh_to_solid(verts, faces, precision=1e-6, remove_splitter=True, method=F face_i = list(face) + [face[0]] face_verts = [Base.Vector(verts[i]) for i in face_i] wire = Part.makePolygon(face_verts) - fc_face = Part.Face(wire) + wire.fixTolerance(precision) + try: + fc_face = Part.Face(wire) + #fc_face = Part.makeFilledFace(wire.Edges) + except Exception as e: + print(f"Face idxs: {face_i}, verts: {face_verts}") + raise Exception("Maybe face is not planar?") from e fc_faces.append(fc_face) shell = Part.makeShell(fc_faces) solid = Part.makeSolid(shell) -- GitLab From d3a5241accfbe0780f008d22d5d35d9c61559291 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 17:40:29 +0500 Subject: [PATCH 32/39] Update documentation. --- docs/nodes/spatial/field_random_probe.rst | 27 ++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/nodes/spatial/field_random_probe.rst b/docs/nodes/spatial/field_random_probe.rst index 0d133148e..da6dfb4dc 100644 --- a/docs/nodes/spatial/field_random_probe.rst +++ b/docs/nodes/spatial/field_random_probe.rst @@ -13,6 +13,15 @@ This node generates random points, which are distributed according to the provid probability of the vertex appearence at some point is proportional to the value of the scalar field at that point. +This node can make sure that generated points are not too close to one another. This can be controlled in one of two ways: + +* By specifying minimum distance between any two different points; +* Or by specifying a radius around each generated points, which should be free. + More precisely, the node makes sure that if you place a sphere of specified + radius at each point as a center, these spheres will never intersect. The + radiuses of such spheres are provided as a scalar field: it defines a radius + value for any point in the space. + Inputs ------ @@ -25,9 +34,13 @@ This node has the following inputs: generated. Only bounding box of these vertices will be used. This input is mandatory. * **Count**. The number of points to be generated. The default value is 50. -* **MinDistance**. Minimum allowable distance between generated points. If set - to zero, there will be no restriction on distance between points. Default - value is 0. +* **MinDistance**. This input is avaliable 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. @@ -74,3 +87,11 @@ Generate cubes according to the scalar field defined by some formula: .. image:: https://user-images.githubusercontent.com/284644/81504488-f94cd080-9302-11ea-9da5-f27f633f2191.png +Example of "Radius Field** mode usage: + +.. image:: https://user-images.githubusercontent.com/284644/102390341-1ca4d000-3ff6-11eb-90f5-dfa07646f941.png + +Here we are placing spheres of different radiuses at each generated point. +Since radiuses of the sphere are defined by the same scalar field which is used +for RadiusField input, these spheres do never intersect. + -- GitLab From f715067f07374b5d3216272cda9f994d5182962a Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 17:50:30 +0500 Subject: [PATCH 33/39] Update documentation. --- docs/nodes/spatial/field_random_probe.rst | 22 ++++++++++-- docs/nodes/spatial/populate_solid.rst | 41 +++++++++++++++++++-- docs/nodes/spatial/populate_surface.rst | 43 ++++++++++++++++++++--- 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/docs/nodes/spatial/field_random_probe.rst b/docs/nodes/spatial/field_random_probe.rst index da6dfb4dc..3c02b70a1 100644 --- a/docs/nodes/spatial/field_random_probe.rst +++ b/docs/nodes/spatial/field_random_probe.rst @@ -13,7 +13,8 @@ This node generates random points, which are distributed according to the provid probability of the vertex appearence at some point is proportional to the value of the scalar field at that point. -This node can make sure that generated points are not too close to one another. This can be controlled in one of two ways: +This node can make sure that generated points are not too close to one another. +This can be controlled in one of two ways: * By specifying minimum distance between any two different points; * Or by specifying a radius around each generated points, which should be free. @@ -57,12 +58,29 @@ This node has the following inputs: Parameters ---------- -This node has the following parameter: +This node has the following parameters: + +* **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 shpere 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**. 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. +* **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. When **Proportional** mode is enabled, then the probability of vertex appearance at the certain point is calculated as ``P = (V - FieldMin) / diff --git a/docs/nodes/spatial/populate_solid.rst b/docs/nodes/spatial/populate_solid.rst index cbec24588..1d472b600 100644 --- a/docs/nodes/spatial/populate_solid.rst +++ b/docs/nodes/spatial/populate_solid.rst @@ -21,6 +21,16 @@ object according to the provided Scalar Field. It has two modes: probability of the vertex appearence at some point is proportional to the value of the scalar field at that point. +This node can make sure that generated points are not too close to one another. +This can be controlled in one of two ways: + +* By specifying minimum distance between any two different points; +* Or by specifying a radius around each generated points, which should be free. + More precisely, the node makes sure that if you place a sphere of specified + radius at each point as a center, these spheres will never intersect. The + radiuses of such spheres are provided as a scalar field: it defines a radius + value for any point in the space. + The node can generate points either inside the Solid body, or on it's surface. Inputs @@ -34,9 +44,13 @@ This node has the following inputs: this input is not connected, the node will generate evenly distributed points. This input is mandatory, if **Proportional** parameter is checked. * **Count**. The number of points to be generated. The default value is 50. -* **MinDistance**. Minimum allowable distance between generated points. If set - to zero, there will be no restriction on distance between points. Default - value is 0. +* **MinDistance**. This input is avaliable 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. @@ -62,10 +76,27 @@ This node has the following parameters: 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 shpere 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**. 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. +* **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. * **Accept in surface**. This parameter is only available when the **Generation mode** parameter is set to **Volume**. This defines whether it is acceptable to generate points on the surface of the body as well as inside it. Checked @@ -92,3 +123,7 @@ Example of Usage .. image:: https://user-images.githubusercontent.com/284644/99146273-5b163a80-2698-11eb-8f1d-a41978cc96ea.png +Example of "Radius Field" mode usage: + +.. image:: https://user-images.githubusercontent.com/284644/102390349-1dd5fd00-3ff6-11eb-98d1-6143b5312380.png + diff --git a/docs/nodes/spatial/populate_surface.rst b/docs/nodes/spatial/populate_surface.rst index 37a83fa9b..34eff9411 100644 --- a/docs/nodes/spatial/populate_surface.rst +++ b/docs/nodes/spatial/populate_surface.rst @@ -14,6 +14,16 @@ according to the provided Scalar Field. It has two modes: probability of the vertex appearence at some point is proportional to the value of the scalar field at that point. +This node can make sure that generated points are not too close to one another. +This can be controlled in one of two ways: + +* By specifying minimum distance between any two different points; +* Or by specifying a radius around each generated points, which should be free. + More precisely, the node makes sure that if you place a sphere of specified + radius at each point as a center, these spheres will never intersect. The + radiuses of such spheres are provided as a scalar field: it defines a radius + value for any point in the space. + Inputs ------ @@ -25,9 +35,13 @@ This node has the following inputs: this input is not connected, the node will generate evenly distributed points. This input is mandatory, if **Proportional** parameter is checked. * **Count**. The number of points to be generated. The default value is 50. -* **MinDistance**. Minimum allowable distance between generated points. If set - to zero, there will be no restriction on distance between points. Default - value is 0. +* **MinDistance**. This input is avaliable 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. @@ -44,12 +58,29 @@ This node has the following inputs: Parameters ---------- -This node has the following parameter: +This node has the following parameters: + +* **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 shpere 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**. 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. +* **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. When **Proportional** mode is enabled, then the probability of vertex appearance at the certain point is calculated as ``P = (V - FieldMin) / @@ -72,3 +103,7 @@ Distribute points on a Moebius band according to some attractor field: .. image:: https://user-images.githubusercontent.com/284644/99146076-7ed88100-2696-11eb-8175-67b5f268f36e.png +Example of "Radius Field" mode usage: + +.. image:: https://user-images.githubusercontent.com/284644/102106806-d5caa500-3e52-11eb-805e-73333b35350c.png + -- GitLab From de128b7c1166ae3b1305beb8e347b5045c4d6d52 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 21:29:36 +0500 Subject: [PATCH 34/39] "Voronoi on mesh" documentation. --- docs/nodes/spatial/spatial_index.rst | 1 + docs/nodes/spatial/voronoi_on_mesh.rst | 78 ++++++++++++++++++++++++++ nodes/spatial/voronoi_on_mesh.py | 9 +-- 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 docs/nodes/spatial/voronoi_on_mesh.rst diff --git a/docs/nodes/spatial/spatial_index.rst b/docs/nodes/spatial/spatial_index.rst index 7427b9ef1..a9aae8e17 100644 --- a/docs/nodes/spatial/spatial_index.rst +++ b/docs/nodes/spatial/spatial_index.rst @@ -17,6 +17,7 @@ Spatial delaunay_2d_cdt voronoi_sphere voronoi_on_surface + voronoi_on_mesh lloyd2d lloyd3d lloyd_on_sphere diff --git a/docs/nodes/spatial/voronoi_on_mesh.rst b/docs/nodes/spatial/voronoi_on_mesh.rst new file mode 100644 index 000000000..ad353b656 --- /dev/null +++ b/docs/nodes/spatial/voronoi_on_mesh.rst @@ -0,0 +1,78 @@ +Voronoi on Mesh +=============== + +Dependencies +------------ + +This node requires SciPy_ library to work. + +.. _SciPy: https://scipy.org/ + +Functionality +------------- + +This node creates Voronoi diagram on a given mesh, from specified set of +initial points (sites). More specifically, it subdivides the mesh into regions +of such Voronoi diagram. It is possible to subdivide: + +* either surface of the mesh, generating a series of flat meshes, +* or the volume of the mesh, generating a series of closed-body meshes. In this + mode, it is required that the mesh represents a closed volume. + +Inputs +------ + +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. +* **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. +* **Spacing**. Percent of space to leave between generated fragment meshes. + Zero means do not leave any space, i.e. regions will fully cover initial + mesh. Maximum possible value is 1.0. The default value is 0. + +Parameters +---------- + +This node has the following parameters: + +* **Mode**. The available options are: + + * **Split Volume**. Split closed-volume mesh into smaller closed-volume mesh regions. + * **Split Surface**. Split the surface of a mesh into smaller flat meshes. + + The default value is **Split Volume**. + +* **Correct normals**. This parameter is available only when **Mode** parameter + is set to **Volume**. If checked, then the node will make sure that all + normals of generated meshes point outside. Otherwise, this is not guaranteed. + Checked by default. +* **Output nesting**. This defines nesting structure of output sockets. The available options are: + + * **Flat list**. Output a single flat list of mesh objects (Voronoi diagram + ridges / regions) for all input meshes. + * **Separate lists**. Output a separate list of mesh objects (Voronoi + diagram ridges / regions) for each input mesh. + * **Join meshes**. Output one mesh, joined from ridges / edges of Voronoi + diagram, for each input mesh. + +* **Accuracy**. This parameter is available in the N panel only. This defines + the precision of mesh calculation (number of digits after decimal point). The + default value is 6. + +Outputs +------- + +This node has the following outputs: + +* **Vertices**. Vertices of generated mesh. +* **Edges**. Edges of generated mesh. +* **Faces**. Faces of generated mesh. + +Example of usage +---------------- + +.. image:: https://user-images.githubusercontent.com/284644/102384062-190d4b00-3fee-11eb-8405-71e8ed383d26.png + diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py index b6dc71625..5a2cb176e 100644 --- a/nodes/spatial/voronoi_on_mesh.py +++ b/nodes/spatial/voronoi_on_mesh.py @@ -100,13 +100,14 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): self.outputs.new('SvVerticesSocket', "Vertices") self.outputs.new('SvStringsSocket', "Edges") self.outputs.new('SvStringsSocket', "Faces") - self.outputs.new('SvVerticesSocket', "AllSites") + #self.outputs.new('SvVerticesSocket', "AllSites") self.update_sockets(context) def draw_buttons(self, context, layout): layout.label(text="Mode:") layout.prop(self, "mode", text='') - layout.prop(self, 'normals') + if self.mode == 'VOLUME': + layout.prop(self, 'normals') layout.label(text='Output nesting:') layout.prop(self, 'join_mode', text='') @@ -152,7 +153,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): do_clip=True, clipping=None, mode = self.mode, precision = precision) - if self.normals: + if self.mode == 'VOLUME' and self.normals: verts, edges, faces = recalc_normals(verts, edges, faces, loop=True) if self.join_mode == 'FLAT': @@ -186,7 +187,7 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): self.outputs['Vertices'].sv_set(verts_out) self.outputs['Edges'].sv_set(edges_out) self.outputs['Faces'].sv_set(faces_out) - self.outputs['AllSites'].sv_set(sites_out) + #self.outputs['AllSites'].sv_set(sites_out) def register(): if scipy is not None: -- GitLab From 5a9925458766860e7d125c162d256616574b807d Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 21:52:13 +0500 Subject: [PATCH 35/39] "Voronoi on solid" documentation. --- docs/nodes/spatial/spatial_index.rst | 1 + docs/nodes/spatial/voronoi_on_mesh.rst | 2 +- nodes/spatial/voronoi_on_solid.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/nodes/spatial/spatial_index.rst b/docs/nodes/spatial/spatial_index.rst index a9aae8e17..8208c4dd8 100644 --- a/docs/nodes/spatial/spatial_index.rst +++ b/docs/nodes/spatial/spatial_index.rst @@ -18,6 +18,7 @@ Spatial voronoi_sphere voronoi_on_surface voronoi_on_mesh + voronoi_on_solid lloyd2d lloyd3d lloyd_on_sphere diff --git a/docs/nodes/spatial/voronoi_on_mesh.rst b/docs/nodes/spatial/voronoi_on_mesh.rst index ad353b656..261627a79 100644 --- a/docs/nodes/spatial/voronoi_on_mesh.rst +++ b/docs/nodes/spatial/voronoi_on_mesh.rst @@ -31,7 +31,7 @@ This node has the following inputs: necessary. This input is mandatory. * **Spacing**. Percent of space to leave between generated fragment meshes. Zero means do not leave any space, i.e. regions will fully cover initial - mesh. Maximum possible value is 1.0. The default value is 0. + mesh. The default value is 0. Parameters ---------- diff --git a/nodes/spatial/voronoi_on_solid.py b/nodes/spatial/voronoi_on_solid.py index 2bf466883..029307671 100644 --- a/nodes/spatial/voronoi_on_solid.py +++ b/nodes/spatial/voronoi_on_solid.py @@ -70,7 +70,7 @@ class SvVoronoiOnSolidNode(bpy.types.Node, SverchCustomTreeNode): inset : FloatProperty( name = "Inset", min = 0.0, max = 1.0, - default = 0.0, + default = 0.1, update = updateNode) flat_output : BoolProperty( -- GitLab From a7c3c9cdb7484c96098b457d3a99053b3906e65e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 21:59:40 +0500 Subject: [PATCH 36/39] Update documentation. --- docs/nodes/spatial/random_points_on_mesh.rst | 16 +++++++++++++--- nodes/spatial/random_points_on_mesh.py | 10 +++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/nodes/spatial/random_points_on_mesh.rst b/docs/nodes/spatial/random_points_on_mesh.rst index a0602f179..54757616d 100644 --- a/docs/nodes/spatial/random_points_on_mesh.rst +++ b/docs/nodes/spatial/random_points_on_mesh.rst @@ -26,8 +26,17 @@ Parameters This node has the following parameters: -- **Proportional**. If checked, then the number of points on each face will be - proportional to the area of the face (and to the weight provided in the +- **Mode**. The available options are: + + * **Surface**. Generate points on the surface of the mesh. + * **Volume**. Generate points inside the volume of the mesh. The mesh is + expected to represent a closed volume in this case. + + The default option is **Surface**. + +- **Proportional**. This parameter is available only when **Mode** parameter is + set to **Surface**. If checked, then the number of points on each face will + be proportional to the area of the face (and to the weight provided in the **Face weight** input). If not checked, then the number of points on each face will be only defined by **Face weight** input. Checked by default. @@ -35,7 +44,8 @@ Outputs ------- - **Verts** - random vertices on mesh -- **Face index** - indexes of faces to which random vertices lays +- **Face index** - indexes of faces to which random vertices lays. This input + is available only when **Mode** parameter is set to **Surface**. Examples -------- diff --git a/nodes/spatial/random_points_on_mesh.py b/nodes/spatial/random_points_on_mesh.py index c5d0de538..c52691018 100644 --- a/nodes/spatial/random_points_on_mesh.py +++ b/nodes/spatial/random_points_on_mesh.py @@ -17,7 +17,7 @@ from mathutils.bvhtree import BVHTree from mathutils.geometry import tessellate_polygon, area_tri from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode +from sverchok.data_structure import updateNode, throttle_and_update_node from sverchok.utils.geom import calc_bounds from sverchok.utils.sv_mesh_utils import point_inside_mesh @@ -185,6 +185,10 @@ class SvRandomPointsOnMesh(bpy.types.Node, SverchCustomTreeNode): default=True, update=updateNode) + @throttle_and_update_node + def update_sockets(self, context): + self.outputs['Face index'].hide_safe = self.mode != 'SURFACE' + modes = [ ('SURFACE', "Surface", "Surface", 0), ('VOLUME', "Volume", "Volume", 1) @@ -194,10 +198,10 @@ class SvRandomPointsOnMesh(bpy.types.Node, SverchCustomTreeNode): name = "Mode", items = modes, default = 'SURFACE', - update=updateNode) + update=update_sockets) def draw_buttons(self, context, layout): - layout.prop(self, "mode") + layout.prop(self, "mode", text='') if self.mode == 'SURFACE': layout.prop(self, "proportional") -- GitLab From d092e63731b6f72aea6ea69d6ae3dba03ffd25cd Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 22:02:19 +0500 Subject: [PATCH 37/39] Edit according to @vicdoval advice. --- nodes/solid/polygon_face.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodes/solid/polygon_face.py b/nodes/solid/polygon_face.py index e54545691..9a3ddbc74 100644 --- a/nodes/solid/polygon_face.py +++ b/nodes/solid/polygon_face.py @@ -53,11 +53,12 @@ class SvSolidPolygonFaceNode(bpy.types.Node, SverchCustomTreeNode): tolerance = 10 ** (-self.accuracy) result = [] + fc_vector = Base.Vector for face_i in face_idxs: face_i = list(face_i) face_i.append(face_i[0]) fc_verts = [verts[idx] for idx in face_i] - fc_verts = [Base.Vector(*vert) for vert in fc_verts] + fc_verts = [fc_vector(*vert) for vert in fc_verts] wire = Part.makePolygon(fc_verts) wire.fixTolerance(tolerance) face = Part.Face(wire) -- GitLab From 1b8f7ca657a082cb3f9fcbcf654ba4fe9c18cd6c Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 22:03:13 +0500 Subject: [PATCH 38/39] Deepcopy=False. --- nodes/spatial/voronoi_on_mesh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nodes/spatial/voronoi_on_mesh.py b/nodes/spatial/voronoi_on_mesh.py index 5a2cb176e..e9566d5bb 100644 --- a/nodes/spatial/voronoi_on_mesh.py +++ b/nodes/spatial/voronoi_on_mesh.py @@ -120,11 +120,11 @@ class SvVoronoiOnMeshNode(bpy.types.Node, SverchCustomTreeNode): if not any(socket.is_linked for socket in self.outputs): return - verts_in = self.inputs['Vertices'].sv_get() - faces_in = self.inputs['Faces'].sv_get() - sites_in = self.inputs['Sites'].sv_get() + 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) #thickness_in = self.inputs['Thickness'].sv_get() - spacing_in = self.inputs['Spacing'].sv_get() + spacing_in = self.inputs['Spacing'].sv_get(deepcopy=False) verts_in = ensure_nesting_level(verts_in, 4) input_level = get_data_nesting_level(sites_in) -- GitLab From d8578a15ec2a89a301235ff919cf06ce87fb17ad Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 27 Dec 2020 22:06:55 +0500 Subject: [PATCH 39/39] Add documentation file. --- docs/nodes/spatial/voronoi_on_solid.rst | 87 +++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/nodes/spatial/voronoi_on_solid.rst diff --git a/docs/nodes/spatial/voronoi_on_solid.rst b/docs/nodes/spatial/voronoi_on_solid.rst new file mode 100644 index 000000000..c2359211e --- /dev/null +++ b/docs/nodes/spatial/voronoi_on_solid.rst @@ -0,0 +1,87 @@ +Voronoi on Solid +================ + +Dependencies +------------ + +This node requires both SciPy_ and FreeCAD_ libraries to work. + +.. _SciPy: https://scipy.org/ +.. _FreeCAD: ../../solids.rst + +Functionality +------------- + +This node creates Voronoi diagram on a given Solid object. The result can be +output as either series of fragments of the shell of Solid object (series of +faces), or as a series of solid bodies. + +**Note**: this node uses FreeCAD's functionality of solid boolean operations +internally. This functionality is known to be slow when working with objects +defined by NURBS surfaces, especially when there are a lot of sites used. Also +please be warned that this functionality is known to cause Blender crashes on +some setups. + +Inputs +------ + +This node has the following inputs: + +* **Solid**. The solid object, on which the Voronoi diagram is to be generated. + This input is mandatory. +* **Sites**. List of points, for which Voronoi diagram is to be generated. This + input is mandatory. +* **Inset**. Percentage of space to leave between generated Voronoi regions. + Zero means the object will be fully covered by generated regions. Maximum + value is 1.0. The default value is 0.1. + +Parameters +---------- + +This node has the following parameters: + +* **Mode**. The available options are available: + + * **Surface**. The node will split the surface (shell) of the solid body into + regions of Voronoi diagram. + * **Volume**. The node will split the volume of the solid body into regions + of Voronoi diagram. + + The default value is **Surface**. + +* **Flat output**. If checked, output single flat list of fragments for all + output solids. Otherwise, output a separate list of fragments for each solid. + Checked by default. +* **Accuracy**. This parameter is available in the N panel only. Precision of + calculations (number of digits after decimal point). The default value is 6. + +Outputs +------- + +This node has the following outputs: + +* **InnerSolid**. Solid objects (or their shells, if **Surface** mode is used) + calculated as regions of Voronoi diagram. +* **OuterSolid**. Solid object, representing the part of volume or shell, which + is left between the regions of Voronoi diagram. This object will be empty if + **Inset** input is set to zero. + +Examples of usage +----------------- + +Inner solids with **Surface** mode: + +.. image:: https://user-images.githubusercontent.com/284644/103175519-411d6980-488c-11eb-86bf-151a4776f6ac.png + +Outer solid for the same setup: + +.. image:: https://user-images.githubusercontent.com/284644/103175520-424e9680-488c-11eb-99aa-d0ea147c29d6.png + +Inner solids with **Volume** mode: + +.. image:: https://user-images.githubusercontent.com/284644/103175523-437fc380-488c-11eb-817b-fe5826d184ed.png + +Outer solid with **Volume** mode: + +.. image:: https://user-images.githubusercontent.com/284644/103175522-42e72d00-488c-11eb-947b-ba57fc3b96f7.png + -- GitLab