From 522198db82f5e240d30137b058c5f2c79daac84f Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 8 Sep 2020 22:55:32 +0500 Subject: [PATCH 1/7] "Select Solid Elements" node. --- index.md | 1 + nodes/solid/solid_select.py | 380 ++++++++++++++++++++++++++++++++++++ utils/solid.py | 166 +++++++++++++++- 3 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 nodes/solid/solid_select.py diff --git a/index.md b/index.md index 8a7f951ad..61755adfc 100644 --- a/index.md +++ b/index.md @@ -241,6 +241,7 @@ SvSolidVerticesNode SvSolidEdgesNode SvSolidFacesNode + SvSelectSolidNode SvSolidViewerNode ## Analyzers diff --git a/nodes/solid/solid_select.py b/nodes/solid/solid_select.py new file mode 100644 index 000000000..1025b5eaf --- /dev/null +++ b/nodes/solid/solid_select.py @@ -0,0 +1,380 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np + +import bpy +from bpy.props import BoolProperty, EnumProperty, FloatVectorProperty, FloatProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import zip_long_repeat, ensure_nesting_level, updateNode +from sverchok.utils.geom import PlaneEquation, LineEquation, linear_approximation +from sverchok.utils.solid import SvSolidTopology +from sverchok.utils.dummy_nodes import add_dummy +from sverchok.dependencies import FreeCAD + +if FreeCAD is None: + add_dummy('SvSelectSolidNode', 'Select Solid Elements', 'FreeCAD') +else: + import FreeCAD + import Part + +class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Select Solid Elements + Tooltip: Select Vertexes, Edges or Faces of Solid object by location + """ + bl_idname = 'SvSelectSolidNode' + bl_label = 'Select Solid Elements' + bl_icon = 'EDGESEL' + solid_catergory = "Operators" + + element_types = [ + ('VERTS', "Vertices", "Select vertices first, and then select adjacent edges and faces", 'VERTEXSEL', 0), + ('EDGES', "Edges", "Select edges first, and then select adjacent vertices and faces", "EDGESEL", 1), + ('FACES', "Faces", "Select faces first, and then select adjacent vertices and edges", 'FACESEL', 2) + ] + + criteria_types = [ + ('SIDE', "By Side", "By Side", 0), + ('NORMAL', "By Normal", "By Normal direction", 1), + ('SPHERE', "By Center and Radius", "By center and radius", 2), + ('PLANE', "By Plane", "By plane defined by point and normal", 3), + ('CYLINDER', "By Cylinder", "By cylinder defined by point, direction vector and radius", 4), + ('DIRECTION', "By Direction", "By direction", 5) + #('BBOX', "By Bounding Box", "By bounding box", 6) + ] + + known_criteria = { + 'VERTS': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER'}, + 'EDGES': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'DIRECTION'}, + 'FACES': {'SIDE', 'NORMAL', 'SPHERE', 'PLANE', 'CYLINDER'} + } + + element_type : EnumProperty( + name = "Select", + description = "What kind of Solid elements to select first", + items = element_types, + default = 'VERTS', + update = updateNode) + + def get_available_criteria(self, context): + result = [] + for item in SvSelectSolidNode.criteria_types: + if item[0] in SvSelectSolidNode.known_criteria[self.element_type]: + result.append(item) + return result + + @throttled + def update_sockets(self, context): + self.inputs['Direction'].hide_safe = self.criteria_type not in {'SIDE', 'NORMAL', 'PLANE', 'CYLINDER', 'DIRECTION'} + self.inputs['Center'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER', 'BBOX'} + self.inputs['Percent'].hide_safe = self.criteria_type not in {'SIDE', 'NORMAL', 'DIRECTION'} + self.inputs['Radius'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER', 'BBOX'} + self.inputs['Precision'].hide_safe = self.element_type == 'VERTS' + + criteria_type : EnumProperty( + name = "Criteria", + description = "Type of criteria to select by", + items = get_available_criteria, + update = update_sockets) + + include_partial: BoolProperty(name="Include partial selection", + description="Include partially selected edges/faces", + default=False, + update=updateNode) + + percent: FloatProperty(name="Percent", + default=1.0, + min=0.0, max=100.0, + update=updateNode) + + radius: FloatProperty(name="Radius", default=1.0, min=0.0, update=updateNode) + + precision: FloatProperty( + name="Precision", + default=0.01, + precision=4, + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'element_type') + layout.prop(self, 'criteria_type', text='') + if self.element_type in {'EDGES', 'FACES'}: + layout.prop(self, 'include_partial') + + def sv_init(self, context): + self.inputs.new('SvSolidSocket', "Solid") + d = self.inputs.new('SvVerticesSocket', "Direction") + d.use_prop = True + d.prop = (0.0, 0.0, 1.0) + + c = self.inputs.new('SvVerticesSocket', "Center") + c.use_prop = True + c.prop = (0.0, 0.0, 0.0) + + self.inputs.new('SvStringsSocket', 'Percent').prop_name = 'percent' + self.inputs.new('SvStringsSocket', 'Radius').prop_name = 'radius' + self.inputs.new('SvStringsSocket', 'Precision').prop_name = 'precision' + + self.outputs.new('SvStringsSocket', 'VerticesMask') + self.outputs.new('SvStringsSocket', 'EdgesMask') + self.outputs.new('SvStringsSocket', 'FacesMask') + + self.update_sockets(context) + + def map_percent(self, values, percent): + maxv = max(values) + minv = min(values) + if maxv <= minv: + return maxv + return maxv - percent * (maxv - minv) * 0.01 + + def _by_side(self, points, direction, percent): + direction = direction / np.linalg.norm(direction) + values = points.dot(direction) + threshold = self.map_percent(values, percent) + return values > threshold + + # VERTS + + def _verts_by_side(self, topo, direction, percent): + verts = [(v.X ,v.Y, v.Z) for v in topo.solid.Vertexes] + verts = np.array(verts) + direction = np.array(direction) + mask = self._by_side(verts, direction, percent) + return mask.tolist() + + def _verts_by_sphere(self, topo, center, radius): + return topo.get_vertices_within_range_mask(center, radius) + + def _verts_by_plane(self, topo, center, direction, radius): + plane = PlaneEquation.from_normal_and_point(direction, center) + condition = lambda v: plane.distance_to_point(v) < radius + return topo.get_vertices_by_location_mask(condition) + + def _verts_by_cylinder(self, topo, center, direction, radius): + line = LineEquation.from_direction_and_point(direction, center) + condition = lambda v: line.distance_to_point(v) < radius + return topo.get_vertices_by_location_mask(condition) + + # EDGES + + def _edges_by_side(self, topo, direction, percent): + direction = np.array(direction) + all_values = [] + values_per_edge = dict() + for edge in topo.solid.Edges: + points = np.array(topo.get_points_by_edge(edge)) + values = points.dot(direction) + all_values.extend(values.tolist()) + values_per_edge[SvSolidTopology.Item(edge)] = values + + threshold = self.map_percent(all_values, percent) + check = any if self.include_partial else all + mask = [] + for edge in topo.solid.Edges: + values = values_per_edge[SvSolidTopology.Item(edge)] + test = check(values > threshold) + mask.append(test) + + return mask + + def _edges_by_sphere(self, topo, center, radius): + center = np.array(center) + def condition(points): + dvs = points - center + distances = (dvs * dvs).sum(axis=1) + return distances < radius*radius + return topo.get_edges_by_location_mask(condition, self.include_partial) + + def _edges_by_plane(self, topo, center, direction, radius): + plane = PlaneEquation.from_normal_and_point(direction, center) + def condition(points): + distances = plane.distance_to_points(points) + return distances < radius + return topo.get_edges_by_location_mask(condition, self.include_partial) + + def _edges_by_cylinder(self, topo, center, direction, radius): + line = LineEquation.from_direction_and_point(direction, center) + def condition(points): + distances = line.distance_to_points(points) + return distances < radius + return topo.get_edges_by_location_mask(condition, self.include_partial) + + def _edges_by_direction(self, topo, direction, percent): + direction = np.array(direction) + + def calc_value(points): + approx = linear_approximation(points) + line = approx.most_similar_line() + line_direction = np.array(line.direction) + return abs(direction.dot(line_direction)) + + values = np.array([calc_value(topo.get_points_by_edge(edge)) for edge in topo.solid.Edges]) + threshold = self.map_percent(values, percent) + return (values > threshold).tolist() + + # FACES + + def _faces_by_side(self, topo, direction, percent): + direction = np.array(direction) + all_values = [] + values_per_face = dict() + for face in topo.solid.Faces: + points = np.array(topo.get_points_by_face(face)) + values = points.dot(direction) + all_values.extend(values.tolist()) + values_per_face[SvSolidTopology.Item(face)] = values + + threshold = self.map_percent(all_values, percent) + check = any if self.include_partial else all + mask = [] + for face in topo.solid.Faces: + values = values_per_face[SvSolidTopology.Item(face)] + test = check(values > threshold) + mask.append(test) + + return mask + + def _faces_by_normal(self, topo, direction, percent): + direction = np.array(direction) + + def calc_value(points): + approx = linear_approximation(points) + plane = approx.most_similar_plane() + plane_normal = np.array(plane.normal) + return direction.dot(plane_normal) + + values = np.array([calc_value(topo.get_points_by_face(face)) for face in topo.solid.Faces]) + threshold = self.map_percent(values, percent) + return (values > threshold).tolist() + + def _faces_by_sphere(self, topo, center, radius): + center = np.array(center) + def condition(points): + dvs = points - center + distances = (dvs * dvs).sum(axis=1) + return distances < radius*radius + return topo.get_faces_by_location_mask(condition, self.include_partial) + + def _faces_by_plane(self, topo, center, direction, radius): + plane = PlaneEquation.from_normal_and_point(direction, center) + def condition(points): + distances = plane.distance_to_points(points) + return distances < radius + return topo.get_faces_by_location_mask(condition, self.include_partial) + + def _faces_by_cylinder(self, topo, center, direction, radius): + line = LineEquation.from_direction_and_point(direction, center) + def condition(points): + distances = line.distance_to_points(points) + return distances < radius + return topo.get_faces_by_location_mask(condition, self.include_partial) + + # SWITCH + + def calc_mask(self, solid, precision, direction, center, percent, radius): + topo = SvSolidTopology(solid) + if self.element_type == 'VERTS': + if self.criteria_type == 'SIDE': + vertex_mask = self._verts_by_side(topo, direction, percent) + elif self.criteria_type == 'SPHERE': + vertex_mask = self._verts_by_sphere(topo, center, radius) + elif self.criteria_type == 'PLANE': + vertex_mask = self._verts_by_plane(topo, center, direction, radius) + elif self.criteria_type == 'CYLINDER': + vertex_mask = self._verts_by_cylinder(topo, center, direction, radius) + else: + raise Exception("Unknown criteria for vertices") + verts = [v for c, v in zip(vertex_mask, solid.Vertexes) if c] + edge_mask = topo.get_edges_by_vertices_mask(verts) + face_mask = topo.get_faces_by_vertices_mask(verts) + elif self.element_type == 'EDGES': + topo.tessellate(precision) + if self.criteria_type == 'SIDE': + edge_mask = self._edges_by_side(topo, direction, percent) + elif self.criteria_type == 'SPHERE': + edge_mask = self._edges_by_sphere(topo, center, radius) + elif self.criteria_type == 'PLANE': + edge_mask = self._edges_by_plane(topo, center, direction, radius) + elif self.criteria_type == 'CYLINDER': + edge_mask = self._edges_by_cylinder(topo, center, direction, radius) + elif self.criteria_type == 'DIRECTION': + edge_mask = self._edges_by_direction(topo, direction, percent) + else: + raise Exception("Unknown criteria for edges") + edges = [e for c, e in zip(edge_mask, solid.Edges) if c] + vertex_mask = topo.get_vertices_by_edges_mask(edges) + face_mask = topo.get_faces_by_edges_mask(edges) + else: # FACES + topo.tessellate(precision) + if self.criteria_type == 'SIDE': + face_mask = self._faces_by_side(topo, direction, percent) + elif self.criteria_type == 'NORMAL': + face_mask = self._faces_by_normal(topo, direction, percent) + elif self.criteria_type == 'SPHERE': + face_mask = self._faces_by_sphere(topo, center, radius) + elif self.criteria_type == 'PLANE': + face_mask = self._faces_by_plane(topo, center, direction, radius) + elif self.criteria_type == 'CYLINDER': + face_mask = self._faces_by_cylinder(topo, center, direction, radius) + else: + raise Exception("Unknown criteria type for faces") + faces = [f for c, f in zip(face_mask, solid.Faces) if c] + vertex_mask = topo.get_vertices_by_faces_mask(faces) + edge_mask = topo.get_edges_by_faces_mask(faces) + + return vertex_mask, edge_mask, face_mask + + def process(self): + + if not any(output.is_linked for output in self.outputs): + return + + solid_s = self.inputs['Solid'].sv_get() + direction_s = self.inputs['Direction'].sv_get() + center_s = self.inputs['Center'].sv_get() + percent_s = self.inputs['Percent'].sv_get() + radius_s = self.inputs['Radius'].sv_get() + precision_s = self.inputs['Precision'].sv_get() + + solid_s = ensure_nesting_level(solid_s, 2, data_types=(Part.Shape,)) + direction_s = ensure_nesting_level(direction_s, 3) + center_s = ensure_nesting_level(center_s, 3) + percent_s = ensure_nesting_level(percent_s, 2) + radius_s = ensure_nesting_level(radius_s, 2) + precision_s = ensure_nesting_level(precision_s, 2) + + vertex_mask_out = [] + edge_mask_out = [] + face_mask_out = [] + for solids, directions, centers, percents, radiuses, precisions in zip_long_repeat(solid_s, direction_s, center_s, percent_s, radius_s, precision_s): + vertex_mask_new = [] + edge_mask_new = [] + face_mask_new = [] + for solid, direction, center, percent, radius, precision in zip_long_repeat(solids, directions, centers, percents, radiuses, precisions): + vertex_mask, edge_mask, face_mask = self.calc_mask(solid, precision, direction, center, percent, radius) + vertex_mask_new.append(vertex_mask) + edge_mask_new.append(edge_mask) + face_mask_new.append(face_mask) + vertex_mask_out.append(vertex_mask_new) + edge_mask_out.append(edge_mask_new) + face_mask_out.append(face_mask_new) + + self.outputs['VerticesMask'].sv_set(vertex_mask_out) + self.outputs['EdgesMask'].sv_set(edge_mask_out) + self.outputs['FacesMask'].sv_set(face_mask_out) + +def register(): + if FreeCAD is not None: + bpy.utils.register_class(SvSelectSolidNode) + +def unregister(): + if FreeCAD is not None: + bpy.utils.unregister_class(SvSelectSolidNode) + diff --git a/utils/solid.py b/utils/solid.py index 7123a68d5..b163cc338 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -4,10 +4,17 @@ # # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE + +import math +from collections import defaultdict +import numpy as np + +from mathutils.kdtree import KDTree + +from sverchok.data_structure import match_long_repeat as mlr from sverchok.dependencies import FreeCAD + if FreeCAD is not None: - import math - from sverchok.data_structure import match_long_repeat as mlr import Part import Mesh @@ -15,6 +22,161 @@ if FreeCAD is not None: from FreeCAD import Base from sverchok.nodes.solid.mesh_to_solid import ensure_triangles + class SvSolidTopology(object): + class Item(object): + def __init__(self, item): + self.item = item + + def __hash__(self): + return self.item.hashCode() + + def __eq__(self, other): + return self.item.isSame(other.item) + + def __repr__(self): + return f"" + + def __init__(self, solid): + self.solid = solid + self._init() + + def __repr__(self): + v = len(self.solid.Vertexes) + e = len(self.solid.Edges) + f = len(self.solid.Faces) + return f"" + + def _init(self): + self._faces_by_vertex = defaultdict(set) + self._faces_by_edge = defaultdict(set) + self._edges_by_vertex = defaultdict(set) + + for face in self.solid.Faces: + for vertex in face.Vertexes: + self._faces_by_vertex[SvSolidTopology.Item(vertex)].add(SvSolidTopology.Item(face)) + for edge in face.Edges: + self._faces_by_edge[SvSolidTopology.Item(edge)].add(SvSolidTopology.Item(face)) + + for edge in self.solid.Edges: + for vertex in edge.Vertexes: + self._edges_by_vertex[SvSolidTopology.Item(vertex)].add(SvSolidTopology.Item(edge)) + + self._tree = KDTree(len(self.solid.Vertexes)) + for i, vertex in enumerate(self.solid.Vertexes): + co = (vertex.X, vertex.Y, vertex.Z) + self._tree.insert(co, i) + self._tree.balance() + + def tessellate(self, precision): + self._points_by_edge = defaultdict(list) + self._points_by_face = defaultdict(list) + + for edge in self.solid.Edges: + points = edge.discretize(Deflection=precision) + i_edge = SvSolidTopology.Item(edge) + for pt in points: + self._points_by_edge[i_edge].append((pt.x, pt.y, pt.z)) + + for face in self.solid.Faces: + data = face.tessellate(precision) + i_face = SvSolidTopology.Item(face) + for pt in data[0]: + self._points_by_face[i_face].append((pt.x, pt.y, pt.z)) + + def get_vertices_by_location(self, condition): + to_tuple = lambda v : (v.X, v.Y, v.Z) + return [to_tuple(v) for v in self.solid.Vertexes if condition(to_tuple(v))] + + def get_vertices_by_location_mask(self, condition): + to_tuple = lambda v : (v.X, v.Y, v.Z) + return [condition(to_tuple(v)) for v in self.solid.Vertexes] + + def get_points_by_edge(self, edge): + return self._points_by_edge[SvSolidTopology.Item(edge)] + + def get_points_by_face(self, face): + return self._points_by_face[SvSolidTopology.Item(face)] + + def get_edges_by_location_mask(self, condition, include_partial): + # condition is vectorized + check = any if include_partial else all + mask = [] + for edge in self.solid.Edges: + test = condition(np.array(self._points_by_edge[SvSolidTopology.Item(edge)])) + mask.append(check(test)) + return mask + + def get_faces_by_location_mask(self, condition, include_partial): + # condition is vectorized + check = any if include_partial else all + mask = [] + for face in self.solid.Faces: + test = condition(np.array(self._points_by_face[SvSolidTopology.Item(face)])) + mask.append(check(test)) + return mask + + def get_faces_by_vertex(self, vertex): + return [i.item for i in self._faces_by_vertex[SvSolidTopology.Item(vertex)]] + + def get_faces_by_vertices_mask(self, vertices): + good = set() + for vertex in vertices: + new = self._faces_by_vertex[SvSolidTopology.Item(vertex)] + good.update(new) + return [SvSolidTopology.Item(face) in good for face in self.solid.Faces] + + def get_faces_by_edge(self, edge): + return [i.item for i in self._faces_by_edge[SvSolidTopology.Item(edge)]] + + def get_faces_by_edges_mask(self, edges): + good = set() + for edge in edges: + new = self._faces_by_edge[SvSolidTopology.Item(edge)] + good.update(new) + return [SvSolidTopology.Item(edge) in good for edge in self.solid.Edges] + + def get_edges_by_vertex(self, vertex): + return [i.item for i in self._edges_by_vertex[SvSolidTopology.Item(vertex)]] + + def get_edges_by_vertices_mask(self, vertices): + good = set() + for vertex in vertices: + new = self._edges_by_vertex[SvSolidTopology.Item(vertex)] + good.update(new) + return [SvSolidTopology.Item(edge) in good for edge in self.solid.Edges] + + def get_edges_by_faces_mask(self, faces): + good = set() + for face in faces: + new = set([SvSolidTopology.Item(e) for e in face.Edges]) + good.update(new) + return [SvSolidTopology.Item(edge) in good for edge in self.solid.Edges] + + def get_vertices_by_faces_mask(self, faces): + good = set() + for face in faces: + new = set([SvSolidTopology.Item(v) for v in face.Vertexes]) + good.update(new) + return [SvSolidTopology.Item(vertex) in good for vertex in self.solid.Vertexes] + + def get_vertices_by_edges_mask(self, edges): + good = set() + for edge in edges: + new = set([SvSolidTopology.Item(v) for v in edge.Vertexes]) + good.update(new) + return [SvSolidTopology.Item(vertex) in good for vertex in self.solid.Vertexes] + + def get_vertices_within_range(self, origin, distance): + found = self._tree.find_range(tuple(origin), distance) + idxs = [item[1] for item in found] + vertices = [self.solid.Vertexes[i] for i in idxs] + return vertices + + def get_vertices_within_range_mask(self, origin, distance): + found = self._tree.find_range(tuple(origin), distance) + idxs = set([item[1] for item in found]) + return [i in idxs for i in range(len(self.solid.Vertexes))] + def basic_mesher(solids, precisions): verts = [] faces = [] -- GitLab From 0700de23893ac49f41d0304d25f4082240d836e2 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 8 Sep 2020 23:52:15 +0500 Subject: [PATCH 2/7] "Distance to Shape" option. --- nodes/solid/solid_select.py | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/nodes/solid/solid_select.py b/nodes/solid/solid_select.py index 1025b5eaf..20e574d16 100644 --- a/nodes/solid/solid_select.py +++ b/nodes/solid/solid_select.py @@ -45,14 +45,15 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): ('SPHERE', "By Center and Radius", "By center and radius", 2), ('PLANE', "By Plane", "By plane defined by point and normal", 3), ('CYLINDER', "By Cylinder", "By cylinder defined by point, direction vector and radius", 4), - ('DIRECTION', "By Direction", "By direction", 5) + ('DIRECTION', "By Direction", "By direction", 5), + ('SOLID_DISTANCE', "By Distance to Solid", "By Distance to Solid", 6) #('BBOX', "By Bounding Box", "By bounding box", 6) ] known_criteria = { - 'VERTS': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER'}, - 'EDGES': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'DIRECTION'}, - 'FACES': {'SIDE', 'NORMAL', 'SPHERE', 'PLANE', 'CYLINDER'} + 'VERTS': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE'}, + 'EDGES': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'DIRECTION', 'SOLID_DISTANCE'}, + 'FACES': {'SIDE', 'NORMAL', 'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE'} } element_type : EnumProperty( @@ -72,10 +73,11 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): @throttled def update_sockets(self, context): self.inputs['Direction'].hide_safe = self.criteria_type not in {'SIDE', 'NORMAL', 'PLANE', 'CYLINDER', 'DIRECTION'} - self.inputs['Center'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER', 'BBOX'} + self.inputs['Center'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER'} self.inputs['Percent'].hide_safe = self.criteria_type not in {'SIDE', 'NORMAL', 'DIRECTION'} - self.inputs['Radius'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER', 'BBOX'} - self.inputs['Precision'].hide_safe = self.element_type == 'VERTS' + self.inputs['Radius'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE'} + self.inputs['Tool'].hide_safe = self.criteria_type not in {'SOLID_DISTANCE'} + self.inputs['Precision'].hide_safe = self.element_type == 'VERTS' or self.criteria_type in {'SOLID_DISTANCE'} criteria_type : EnumProperty( name = "Criteria", @@ -104,11 +106,12 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons(self, context, layout): layout.prop(self, 'element_type') layout.prop(self, 'criteria_type', text='') - if self.element_type in {'EDGES', 'FACES'}: + if self.element_type in {'EDGES', 'FACES'} and self.criteria_type not in {'SOLID_DISTANCE'}: layout.prop(self, 'include_partial') def sv_init(self, context): self.inputs.new('SvSolidSocket', "Solid") + self.inputs.new('SvSolidSocket', "Tool") d = self.inputs.new('SvVerticesSocket', "Direction") d.use_prop = True d.prop = (0.0, 0.0, 1.0) @@ -162,6 +165,11 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): condition = lambda v: line.distance_to_point(v) < radius return topo.get_vertices_by_location_mask(condition) + def _verts_by_solid_distance(self, topo, tool, radius): + condition = lambda v: v.distToShape(tool)[0] < radius + mask = [condition(v) for v in topo.solid.Vertexes] + return mask + # EDGES def _edges_by_side(self, topo, direction, percent): @@ -219,6 +227,11 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): threshold = self.map_percent(values, percent) return (values > threshold).tolist() + def _edges_by_solid_distance(self, topo, tool, radius): + condition = lambda e: e.distToShape(tool)[0] < radius + mask = [condition(e) for e in topo.solid.Edges] + return mask + # FACES def _faces_by_side(self, topo, direction, percent): @@ -276,9 +289,14 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): return distances < radius return topo.get_faces_by_location_mask(condition, self.include_partial) + def _faces_by_solid_distance(self, topo, tool, radius): + condition = lambda f: f.distToShape(tool)[0] < radius + mask = [condition(f) for f in topo.solid.Faces] + return mask + # SWITCH - def calc_mask(self, solid, precision, direction, center, percent, radius): + def calc_mask(self, solid, tool, precision, direction, center, percent, radius): topo = SvSolidTopology(solid) if self.element_type == 'VERTS': if self.criteria_type == 'SIDE': @@ -289,6 +307,8 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): vertex_mask = self._verts_by_plane(topo, center, direction, radius) elif self.criteria_type == 'CYLINDER': vertex_mask = self._verts_by_cylinder(topo, center, direction, radius) + elif self.criteria_type == 'SOLID_DISTANCE': + vertex_mask = self._verts_by_solid_distance(topo, tool, radius) else: raise Exception("Unknown criteria for vertices") verts = [v for c, v in zip(vertex_mask, solid.Vertexes) if c] @@ -306,6 +326,8 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): edge_mask = self._edges_by_cylinder(topo, center, direction, radius) elif self.criteria_type == 'DIRECTION': edge_mask = self._edges_by_direction(topo, direction, percent) + elif self.criteria_type == 'SOLID_DISTANCE': + edge_mask = self._edges_by_solid_distance(topo, tool, radius) else: raise Exception("Unknown criteria for edges") edges = [e for c, e in zip(edge_mask, solid.Edges) if c] @@ -323,6 +345,8 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): face_mask = self._faces_by_plane(topo, center, direction, radius) elif self.criteria_type == 'CYLINDER': face_mask = self._faces_by_cylinder(topo, center, direction, radius) + elif self.criteria_mask == 'SOLID_DISTANCE': + face_mask = self._faces_by_solid_distance(topo, tool, radius) else: raise Exception("Unknown criteria type for faces") faces = [f for c, f in zip(face_mask, solid.Faces) if c] @@ -337,6 +361,11 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): return solid_s = self.inputs['Solid'].sv_get() + if self.criteria_type in {'SOLID_DISTANCE'}: + tool_s = self.inputs['Tool'].sv_get() + tool_s = ensure_nesting_level(tool_s, 2, data_types=(Part.Shape,)) + else: + tool_s = [[None]] direction_s = self.inputs['Direction'].sv_get() center_s = self.inputs['Center'].sv_get() percent_s = self.inputs['Percent'].sv_get() @@ -353,12 +382,12 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): vertex_mask_out = [] edge_mask_out = [] face_mask_out = [] - for solids, directions, centers, percents, radiuses, precisions in zip_long_repeat(solid_s, direction_s, center_s, percent_s, radius_s, precision_s): + for objects in zip_long_repeat(solid_s, tool_s, direction_s, center_s, percent_s, radius_s, precision_s): vertex_mask_new = [] edge_mask_new = [] face_mask_new = [] - for solid, direction, center, percent, radius, precision in zip_long_repeat(solids, directions, centers, percents, radiuses, precisions): - vertex_mask, edge_mask, face_mask = self.calc_mask(solid, precision, direction, center, percent, radius) + for solid, tool, direction, center, percent, radius, precision in zip_long_repeat(*objects): + vertex_mask, edge_mask, face_mask = self.calc_mask(solid, tool, precision, direction, center, percent, radius) vertex_mask_new.append(vertex_mask) edge_mask_new.append(edge_mask) face_mask_new.append(face_mask) -- GitLab From 674a4ddd17a67f92fc6e97f12d22e1b7c1b90b64 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 9 Sep 2020 21:32:58 +0500 Subject: [PATCH 3/7] On "face by normal" mode. --- nodes/solid/solid_select.py | 10 ++++------ utils/solid.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/nodes/solid/solid_select.py b/nodes/solid/solid_select.py index 20e574d16..cf3c9687e 100644 --- a/nodes/solid/solid_select.py +++ b/nodes/solid/solid_select.py @@ -257,13 +257,10 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): def _faces_by_normal(self, topo, direction, percent): direction = np.array(direction) - def calc_value(points): - approx = linear_approximation(points) - plane = approx.most_similar_plane() - plane_normal = np.array(plane.normal) - return direction.dot(plane_normal) + def calc_value(normal): + return direction.dot(normal) - values = np.array([calc_value(topo.get_points_by_face(face)) for face in topo.solid.Faces]) + values = np.array([calc_value(topo.get_normal_by_face(face)) for face in topo.solid.Faces]) threshold = self.map_percent(values, percent) return (values > threshold).tolist() @@ -338,6 +335,7 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): if self.criteria_type == 'SIDE': face_mask = self._faces_by_side(topo, direction, percent) elif self.criteria_type == 'NORMAL': + topo.calc_normals() face_mask = self._faces_by_normal(topo, direction, percent) elif self.criteria_type == 'SPHERE': face_mask = self._faces_by_sphere(topo, center, radius) diff --git a/utils/solid.py b/utils/solid.py index b163cc338..2f0a6180a 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -83,6 +83,22 @@ if FreeCAD is not None: for pt in data[0]: self._points_by_face[i_face].append((pt.x, pt.y, pt.z)) + def calc_normals(self): + self._normals_by_face = dict() + for face in self.solid.Faces: + #face.tessellate(precision) + #u_min, u_max, v_min, v_max = face.ParameterRange + sum_normal = Base.Vector(0,0,0) + for u, v in face.getUVNodes(): + normal = face.normalAt(u,v) + sum_normal = sum_normal + normal + sum_normal = np.array([sum_normal.x, sum_normal.y, sum_normal.z]) + sum_normal = sum_normal / np.linalg.norm(sum_normal) + self._normals_by_face[SvSolidTopology.Item(face)] = sum_normal + + def get_normal_by_face(self, face): + return self._normals_by_face[SvSolidTopology.Item(face)] + def get_vertices_by_location(self, condition): to_tuple = lambda v : (v.X, v.Y, v.Z) return [to_tuple(v) for v in self.solid.Vertexes if condition(to_tuple(v))] -- GitLab From 6fef42571e44d8f5e777168525dfffc76e66c11f Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 11 Sep 2020 18:29:49 +0500 Subject: [PATCH 4/7] Support "Inside Solid" mode. --- nodes/solid/solid_select.py | 61 +++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/nodes/solid/solid_select.py b/nodes/solid/solid_select.py index cf3c9687e..9403656d3 100644 --- a/nodes/solid/solid_select.py +++ b/nodes/solid/solid_select.py @@ -22,6 +22,7 @@ if FreeCAD is None: else: import FreeCAD import Part + from FreeCAD import Base class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): """ @@ -46,16 +47,26 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): ('PLANE', "By Plane", "By plane defined by point and normal", 3), ('CYLINDER', "By Cylinder", "By cylinder defined by point, direction vector and radius", 4), ('DIRECTION', "By Direction", "By direction", 5), - ('SOLID_DISTANCE', "By Distance to Solid", "By Distance to Solid", 6) + ('SOLID_DISTANCE', "By Distance to Solid", "By Distance to Solid", 6), + ('SOLID_INSIDE', "Inside Solid", "Select elements that are inside given solid", 7) #('BBOX', "By Bounding Box", "By bounding box", 6) ] known_criteria = { - 'VERTS': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE'}, - 'EDGES': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'DIRECTION', 'SOLID_DISTANCE'}, - 'FACES': {'SIDE', 'NORMAL', 'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE'} + 'VERTS': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE', 'SOLID_INSIDE'}, + 'EDGES': {'SIDE', 'SPHERE', 'PLANE', 'CYLINDER', 'DIRECTION', 'SOLID_DISTANCE', 'SOLID_INSIDE'}, + 'FACES': {'SIDE', 'NORMAL', 'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE', 'SOLID_INSIDE'} } + @throttled + def update_type(self, context): + criteria = self.criteria_type + available = SvSelectSolidNode.known_criteria[self.element_type] + if criteria not in available: + self.criteria_type = available[0] + else: + self.update_sockets(context) + element_type : EnumProperty( name = "Select", description = "What kind of Solid elements to select first", @@ -76,7 +87,7 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): self.inputs['Center'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER'} self.inputs['Percent'].hide_safe = self.criteria_type not in {'SIDE', 'NORMAL', 'DIRECTION'} self.inputs['Radius'].hide_safe = self.criteria_type not in {'SPHERE', 'PLANE', 'CYLINDER', 'SOLID_DISTANCE'} - self.inputs['Tool'].hide_safe = self.criteria_type not in {'SOLID_DISTANCE'} + self.inputs['Tool'].hide_safe = self.criteria_type not in {'SOLID_DISTANCE', 'SOLID_INSIDE'} self.inputs['Precision'].hide_safe = self.element_type == 'VERTS' or self.criteria_type in {'SOLID_DISTANCE'} criteria_type : EnumProperty( @@ -103,11 +114,26 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): precision=4, update=updateNode) + tolerance : FloatProperty( + name = "Tolerance", + default = 0.01, + precision = 4, + update=updateNode) + + include_shell : BoolProperty( + name = "Including shell", + description = "indicates if the point lying directly on a face is considered to be inside or not", + default = False, + update=updateNode) + def draw_buttons(self, context, layout): layout.prop(self, 'element_type') layout.prop(self, 'criteria_type', text='') if self.element_type in {'EDGES', 'FACES'} and self.criteria_type not in {'SOLID_DISTANCE'}: layout.prop(self, 'include_partial') + if self.criteria_type == 'SOLID_INSIDE': + layout.prop(self, 'tolerance') + layout.prop(self, 'include_shell') def sv_init(self, context): self.inputs.new('SvSolidSocket', "Solid") @@ -170,6 +196,11 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): mask = [condition(v) for v in topo.solid.Vertexes] return mask + def _verts_by_solid_inside(self, topo, tool): + condition = lambda v: tool.isInside(v.Point, self.tolerance, self.include_shell) + mask = [condition(v) for v in topo.solid.Vertexes] + return mask + # EDGES def _edges_by_side(self, topo, direction, percent): @@ -232,6 +263,12 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): mask = [condition(e) for e in topo.solid.Edges] return mask + def _edges_by_solid_inside(self, topo, tool): + def condition(points): + good = [tool.isInside(Base.Vector(*p), self.tolerance, self.include_shell) for p in points] + return np.array(good) + return topo.get_edges_by_location_mask(condition, self.include_partial) + # FACES def _faces_by_side(self, topo, direction, percent): @@ -291,6 +328,12 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): mask = [condition(f) for f in topo.solid.Faces] return mask + def _faces_by_solid_inside(self, topo, tool): + def condition(points): + good = [tool.isInside(Base.Vector(*p), self.tolerance, self.include_shell) for p in points] + return np.array(good) + return topo.get_faces_by_location_mask(condition, self.include_partial) + # SWITCH def calc_mask(self, solid, tool, precision, direction, center, percent, radius): @@ -306,6 +349,8 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): vertex_mask = self._verts_by_cylinder(topo, center, direction, radius) elif self.criteria_type == 'SOLID_DISTANCE': vertex_mask = self._verts_by_solid_distance(topo, tool, radius) + elif self.criteria_type == 'SOLID_INSIDE': + vertex_mask = self._verts_by_solid_inside(topo, tool) else: raise Exception("Unknown criteria for vertices") verts = [v for c, v in zip(vertex_mask, solid.Vertexes) if c] @@ -325,6 +370,8 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): edge_mask = self._edges_by_direction(topo, direction, percent) elif self.criteria_type == 'SOLID_DISTANCE': edge_mask = self._edges_by_solid_distance(topo, tool, radius) + elif self.criteria_type == 'SOLID_INSIDE': + edge_mask = self._edges_by_solid_inside(topo, tool) else: raise Exception("Unknown criteria for edges") edges = [e for c, e in zip(edge_mask, solid.Edges) if c] @@ -345,6 +392,8 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): face_mask = self._faces_by_cylinder(topo, center, direction, radius) elif self.criteria_mask == 'SOLID_DISTANCE': face_mask = self._faces_by_solid_distance(topo, tool, radius) + elif self.criteria_type == 'SOLID_INSIDE': + face_mask = self._faces_by_solid_inside(topo, tool) else: raise Exception("Unknown criteria type for faces") faces = [f for c, f in zip(face_mask, solid.Faces) if c] @@ -359,7 +408,7 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): return solid_s = self.inputs['Solid'].sv_get() - if self.criteria_type in {'SOLID_DISTANCE'}: + if self.criteria_type in {'SOLID_DISTANCE', 'SOLID_INSIDE'}: tool_s = self.inputs['Tool'].sv_get() tool_s = ensure_nesting_level(tool_s, 2, data_types=(Part.Shape,)) else: -- GitLab From a2cbf461d600315525615fb29dc123aff5948ec2 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 11 Sep 2020 19:37:54 +0500 Subject: [PATCH 5/7] Secondary "partial selection" support. --- nodes/solid/solid_select.py | 43 ++++++++++++++++++++------ utils/solid.py | 60 ++++++++++++++++++++++++++----------- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/nodes/solid/solid_select.py b/nodes/solid/solid_select.py index 9403656d3..89c5c36f7 100644 --- a/nodes/solid/solid_select.py +++ b/nodes/solid/solid_select.py @@ -11,7 +11,7 @@ import bpy from bpy.props import BoolProperty, EnumProperty, FloatVectorProperty, FloatProperty from sverchok.node_tree import SverchCustomTreeNode, throttled -from sverchok.data_structure import zip_long_repeat, ensure_nesting_level, updateNode +from sverchok.data_structure import zip_long_repeat, ensure_nesting_level, updateNode, get_data_nesting_level from sverchok.utils.geom import PlaneEquation, LineEquation, linear_approximation from sverchok.utils.solid import SvSolidTopology from sverchok.utils.dummy_nodes import add_dummy @@ -97,7 +97,12 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): update = update_sockets) include_partial: BoolProperty(name="Include partial selection", - description="Include partially selected edges/faces", + description="Include partially selected edges/faces - for primary selection", + default=False, + update=updateNode) + + include_partial_other: BoolProperty(name="Include partial selection", + description="Include partially selected vertices/edges/faces - for secondary selection", default=False, update=updateNode) @@ -129,8 +134,21 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons(self, context, layout): layout.prop(self, 'element_type') layout.prop(self, 'criteria_type', text='') + if self.element_type in {'EDGES', 'FACES'} and self.criteria_type not in {'SOLID_DISTANCE'}: - layout.prop(self, 'include_partial') + if self.element_type == 'EDGES': + text = "Partially selected edges" + else: + text = "Partially selected faces" + layout.prop(self, 'include_partial', text=text) + + if self.element_type == 'VERTS': + text = "Partially selected edges, faces" + layout.prop(self, 'include_partial_other', text=text) + elif self.element_type == 'EDGES': + text = "Partially selected faces" + layout.prop(self, 'include_partial_other', text=text) + if self.criteria_type == 'SOLID_INSIDE': layout.prop(self, 'tolerance') layout.prop(self, 'include_shell') @@ -354,8 +372,8 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): else: raise Exception("Unknown criteria for vertices") verts = [v for c, v in zip(vertex_mask, solid.Vertexes) if c] - edge_mask = topo.get_edges_by_vertices_mask(verts) - face_mask = topo.get_faces_by_vertices_mask(verts) + edge_mask = topo.get_edges_by_vertices_mask(verts, self.include_partial_other) + face_mask = topo.get_faces_by_vertices_mask(verts, self.include_partial_other) elif self.element_type == 'EDGES': topo.tessellate(precision) if self.criteria_type == 'SIDE': @@ -376,7 +394,7 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): raise Exception("Unknown criteria for edges") edges = [e for c, e in zip(edge_mask, solid.Edges) if c] vertex_mask = topo.get_vertices_by_edges_mask(edges) - face_mask = topo.get_faces_by_edges_mask(edges) + face_mask = topo.get_faces_by_edges_mask(edges, self.include_partial_other) else: # FACES topo.tessellate(precision) if self.criteria_type == 'SIDE': @@ -419,6 +437,7 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): radius_s = self.inputs['Radius'].sv_get() precision_s = self.inputs['Precision'].sv_get() + input_level = get_data_nesting_level(solid_s, data_types=(Part.Shape,)) solid_s = ensure_nesting_level(solid_s, 2, data_types=(Part.Shape,)) direction_s = ensure_nesting_level(direction_s, 3) center_s = ensure_nesting_level(center_s, 3) @@ -438,9 +457,15 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): vertex_mask_new.append(vertex_mask) edge_mask_new.append(edge_mask) face_mask_new.append(face_mask) - vertex_mask_out.append(vertex_mask_new) - edge_mask_out.append(edge_mask_new) - face_mask_out.append(face_mask_new) + + if input_level == 2: + vertex_mask_out.append(vertex_mask_new) + edge_mask_out.append(edge_mask_new) + face_mask_out.append(face_mask_new) + else: + vertex_mask_out.extend(vertex_mask_new) + edge_mask_out.extend(edge_mask_new) + face_mask_out.extend(face_mask_new) self.outputs['VerticesMask'].sv_set(vertex_mask_out) self.outputs['EdgesMask'].sv_set(edge_mask_out) diff --git a/utils/solid.py b/utils/solid.py index 2f0a6180a..3a95772ad 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -134,32 +134,56 @@ if FreeCAD is not None: def get_faces_by_vertex(self, vertex): return [i.item for i in self._faces_by_vertex[SvSolidTopology.Item(vertex)]] - def get_faces_by_vertices_mask(self, vertices): - good = set() - for vertex in vertices: - new = self._faces_by_vertex[SvSolidTopology.Item(vertex)] - good.update(new) - return [SvSolidTopology.Item(face) in good for face in self.solid.Faces] + def get_faces_by_vertices_mask(self, vertices, include_partial=True): + if include_partial: + good = set() + for vertex in vertices: + new = self._faces_by_vertex[SvSolidTopology.Item(vertex)] + good.update(new) + return [SvSolidTopology.Item(face) in good for face in self.solid.Faces] + else: + vertices = set([SvSolidTopology.Item(v) for v in vertices]) + mask = [] + for face in self.solid.Faces: + ok = all(SvSolidTopology.Item(v) in vertices for v in face.Vertexes) + mask.append(ok) + return mask def get_faces_by_edge(self, edge): return [i.item for i in self._faces_by_edge[SvSolidTopology.Item(edge)]] - def get_faces_by_edges_mask(self, edges): - good = set() - for edge in edges: - new = self._faces_by_edge[SvSolidTopology.Item(edge)] - good.update(new) - return [SvSolidTopology.Item(edge) in good for edge in self.solid.Edges] + def get_faces_by_edges_mask(self, edges, include_partial=True): + if include_partial: + good = set() + for edge in edges: + new = self._faces_by_edge[SvSolidTopology.Item(edge)] + good.update(new) + return [SvSolidTopology.Item(edge) in good for edge in self.solid.Edges] + else: + edges = set([SvSolidTopology.Item(e) for e in edges]) + mask = [] + for face in self.solid.Faces: + ok = all(SvSolidTopology.Item(e) in edges for e in face.Edges) + mask.append(ok) + return mask def get_edges_by_vertex(self, vertex): return [i.item for i in self._edges_by_vertex[SvSolidTopology.Item(vertex)]] - def get_edges_by_vertices_mask(self, vertices): - good = set() - for vertex in vertices: - new = self._edges_by_vertex[SvSolidTopology.Item(vertex)] - good.update(new) - return [SvSolidTopology.Item(edge) in good for edge in self.solid.Edges] + def get_edges_by_vertices_mask(self, vertices, include_partial=True): + if include_partial: + good = set() + for vertex in vertices: + new = self._edges_by_vertex[SvSolidTopology.Item(vertex)] + good.update(new) + return [SvSolidTopology.Item(edge) in good for edge in self.solid.Edges] + else: + vertices = set([SvSolidTopology.Item(v) for v in vertices]) + mask = [] + for edge in self.solid.Edges: + ok = all(SvSolidTopology.Item(v) in vertices for v in edge.Vertexes) + mask.append(ok) + return mask def get_edges_by_faces_mask(self, faces): good = set() -- GitLab From e443b6ca7c2d759eba0bdf6fe3d7b9b9aa5e8aa2 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 11 Sep 2020 20:35:43 +0500 Subject: [PATCH 6/7] Add documentation. --- docs/nodes/solid/solid_index.rst | 1 + docs/nodes/solid/solid_select.rst | 185 ++++++++++++++++++++++++++++++ nodes/solid/solid_select.py | 8 +- 3 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 docs/nodes/solid/solid_select.rst diff --git a/docs/nodes/solid/solid_index.rst b/docs/nodes/solid/solid_index.rst index c9e204bce..c3300d931 100644 --- a/docs/nodes/solid/solid_index.rst +++ b/docs/nodes/solid/solid_index.rst @@ -32,6 +32,7 @@ Solid solid_faces solid_edges solid_vertices + solid_select import_solid export_solid solid_viewer diff --git a/docs/nodes/solid/solid_select.rst b/docs/nodes/solid/solid_select.rst new file mode 100644 index 000000000..2097dd9ab --- /dev/null +++ b/docs/nodes/solid/solid_select.rst @@ -0,0 +1,185 @@ +Select Solid Elements +===================== + +Dependencies +------------ + +This node requires FreeCAD_ library to work. + +.. _FreeCAD: ../../solids.rst + +Functionality +------------- + +This node allows to select topological elements of Solid object (vertices, +edges, or faces) by different geometrical criteria. The node is similar to +"Select Mesh Elements by Location", but works with elements of Solid objects +instead of meshes. + +One has to understand, that edges and faces of Solid objects can be far from +planar, so meanings of "edge direction" and "face normal" are very imprecise in +complex cases. However, since this node knows many ways of selecting things, it +is in most cases possible to select what you want. Also, don't forget that +several masks can be combined with "Logic" node. + +The node works in two steps: + +1. "Primary" selection. Vertices, edges, or faces (depending on **Select** + parameter) are selected according to specified criteria. When selecting + vertices, everything is more or less simple. When selecting edges or faces, + it can appear that part of edge / face does conform to the condition, while + other part does not. For example, it can appear that only central part of + some face does belong to the sphere defined in selection criteria. The node + has a parameter which defines whether such "partially selected" elements + should be considered as selected or not. + + When selecting edges or faces at the primary selection step, the node has to + calculate some number of points on these edges / faces, to check required + geometrical conditions for these points. The node has a parameter to define + how many points will be generated (more points leads to more precise + results, but take more time). + +2. "Secondary" selection. The node considers elements adjacent to the elements + which were selected at the first step. For example, if at first step we + selected some faces, then let's consider edges of those faces. Elements + adjacent to "primary selection" are considered as selected. Depending on + what type of elements were used at the first step, there also can be + "partially selected" elements: + + * If at first step we selected vertices, then there will be some edges and + faces, which have only some of their vertices selected; + * If at first step we selected edges, then there will be some faces, which + have only some of their edges selected. + + The node has a parameter to define whether such "partially selected" + elements should be considered as selected or not. + +For example, if **Select** parameter is set to **Vertices**, then +**VerticesMask** will contain selection mask for vertices selected at "primary +selection" step, while **EdgesMask** and **FacesMask** will contain selection +masks for edges and faces selected at "secondary selection" step. + +Inputs +------ + +This node has the following inputs: + +* **Solid**. The solid object to be considered. This input is mandatory. +* **Tool**. The secondary solid object to be used for **By Distance to Solid**, + **Inside Solid** selection modes. This input is available and mandatory only + for these values of **Criteria** parameter. +* **Direction**. Exact meaning of this input depends on **Criteria** parameter: + + * For **By Side** mode, this is the vector pointing to the side you want to select; + * For **By Normal** mode, this is the direction of normals; + * For **By Plane** mode, this is the normal vector of the plane; + * For **By Cylinder** mode, this is the directing vector of the cylinder; + * For **By Direction** mode, this is the direction of the edges. + + This input is not available for other modes. The default value is ``(0, 0, 1)``. + +* **Center**. Exact meaning of this input depends on **Criteria** parameter: + + * For **By Center and Radius** mode, this is the center of the selecting sphere; + * For **By Plane** mode, this is a point on the selecting plane; + * For **By Cylinder** mode, this is a point on the axis of selecting cylinder. + + This input is not available for other modes. The default value is ``(0, 0, 0)``. + +* **Percent**. This defines how many elements are to be selected. Available for + **By Side**, **By Normal**, **By Direction** modes. The default value is 1.0. +* **Radius**. Radius of the selection area. Exact meaning depends on **Criteria** parameter: + + * For **By Center and Radius** mode, this is the radius of selecting sphere; + * For **By Plane** mode, this is the maximum distance from the plane for + element to be selected; + * For **By Cylinder** mode, this is the radius of selecting cylinder; + * For **By Distance to Solid**, this is the maximum distance from the "tool" + solid for element to be selected. + + This input is not available for other modes. The default value is 1.0. +* **Precision**. Tessellation precision for selecting edges or faces. Smaller + values will generate more points on edges / faces, and so give more precise + results, but will take more time to calculate. This input is available only + when **Select** parameter is set to **Edges** or **Faces**. The default value + is 0.01. + +Parameters +---------- + +This node has the following parameters: + +* **Select**. This defines the type of elements to be selected at primary + selection step. The available values are **Vertices**, **Edges** and + **Faces**. The default value is **Vertices**. +* **Criteria**. This defines the type of geometrical criteria to be used for + primary selection. Not all types of criteria are available for all types of + primary elements. The available types are: + + * **By Side**. Selects elements that are located at one side of the object. + The side is specified by **Direction** input. So, you can select + "rightmost** vertices by passing ``(1, 0, 0)`` as Direction. Number of + elements to select is controlled by **Percent** input: 1% means select + only "most rightmost" elements, 99% means select "all but most leftmost". + More exactly, this mode selects point V if ``(Direction, V) >= max - + Percent * (max - min)``, where `max` and `min` are the maximum and minimum + values of that scalar product amongst all points being considered. + * **By Normal**. This mode is available only when **Select** parameter is + set to **Faces**. Selects faces, that have normal vectors pointing in the + specified **Direction**. So you can select "faces looking to right". More + exactly, this mode selects face F if ``(Direction, Normal(F)) >= max - + Percent * (max - min)``, where `max` and `min` are the maximum and minimum + values of that scalar product amongst all faces. For non-planar face, it's + normal is calculated by calculating normals at many points of that face, + and then averaging them. + * **By Center and Radius**. Selects elements, which are within **Radius** + from the specified **Center**. + * **By Plane**. Selects elements, which are within **Radius** from the (infinite) + plane, defined by **Center** point and **Direction** normal vector. + * **By Cylinder**. Selects elements, which are within **Radius** from the + (infinite) straight line, defined by **Center** point and **Direction** + directing vector. + * **By Direction**. This mode is available only when **Select** parameter is + set to **Edges**. Selectsedges, which are nearly parallel to the specified + **Direction** vector. Note that this mode considers edges as non-directed; + as a result, you can change sign of all coordinates of **Direction**, and + this will not affect output. More exactly, this mode selects edge E if + `Abs(Cos(Angle(E, Direction))) >= max - Percent * (max - min)`, where max + and min are maximum and minimum values of that cosine. For non-linear + edges, the direction is caluclated by taking some number of points of this + edge, and then approximating them by a straight line. + * **By Distance to Solid**. Selects elements, which are within **Radius** + from the second Solid object, provided in the **Tool** input. + * **Inside Solid**. Selects elements, which are inside of the second Solid + object, provided in the **Tool** input. + +* **1. Partially selected edges, faces**. At primary selection step, this + defines whether the node should consider edges or faces, that have only part + of their points conforming to the condition, as selected. This parameter is + not available when the **Select** parameter is set to **Vertices**. Unchecked by default. +* **2. Partially selected edges, faces**. At secondary selection step, this + defines whether the node should consider edges or faces, that have only part + of their vertices / edges selected at the primary step, as selected. Unchecked by default. +* **Tolerance**. This parameter is available only when **Criteria** parameter + is set to **Inside Solid**. This defines the precision of calculation. + Smaller values give more precise results, but take more time. The default + value is 0.01. +* **Including shell**. This parameter is available only when **Criteria** + parameter is set to **Inside Solid**. This defines, whether points lying + directly on a face of the Tool solid, are to be considered as selected. + Unchecked by default. + +Outputs +------- + +This node has the following outputs: + +* **VerticesMask**. Mask for selected vertices. +* **EdgesMask**. Mask for selected edges. +* **FacesMask**. Mask for selected faces. + +Example of usage +---------------- + +.. image:: https://user-images.githubusercontent.com/284644/92938146-a2e4cf80-f465-11ea-9e9b-d938dd9dd629.png + diff --git a/nodes/solid/solid_select.py b/nodes/solid/solid_select.py index 89c5c36f7..281bcf694 100644 --- a/nodes/solid/solid_select.py +++ b/nodes/solid/solid_select.py @@ -137,16 +137,16 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): if self.element_type in {'EDGES', 'FACES'} and self.criteria_type not in {'SOLID_DISTANCE'}: if self.element_type == 'EDGES': - text = "Partially selected edges" + text = "1. Partially selected edges" else: - text = "Partially selected faces" + text = "1. Partially selected faces" layout.prop(self, 'include_partial', text=text) if self.element_type == 'VERTS': - text = "Partially selected edges, faces" + text = "2. Partially selected edges, faces" layout.prop(self, 'include_partial_other', text=text) elif self.element_type == 'EDGES': - text = "Partially selected faces" + text = "2. Partially selected faces" layout.prop(self, 'include_partial_other', text=text) if self.criteria_type == 'SOLID_INSIDE': -- GitLab From f8ffb49e7e84628f6ee429ce3a7bc0b4f33523ba Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 11 Sep 2020 20:36:46 +0500 Subject: [PATCH 7/7] Node icon. --- nodes/solid/solid_select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/solid/solid_select.py b/nodes/solid/solid_select.py index 281bcf694..05014bbda 100644 --- a/nodes/solid/solid_select.py +++ b/nodes/solid/solid_select.py @@ -31,7 +31,7 @@ class SvSelectSolidNode(bpy.types.Node, SverchCustomTreeNode): """ bl_idname = 'SvSelectSolidNode' bl_label = 'Select Solid Elements' - bl_icon = 'EDGESEL' + bl_icon = 'UV_SYNC_SELECT' solid_catergory = "Operators" element_types = [ -- GitLab