diff --git a/docs/nodes/solid/solid_index.rst b/docs/nodes/solid/solid_index.rst index c9e204bce2b3cc02ab42ac1132d26133910b96a0..c3300d9310976dee175f3d336ac5a4d52237b6b6 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 0000000000000000000000000000000000000000..2097dd9ab4e5dfe0e1c0cc47ee7e427135162432 --- /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/index.md b/index.md index 8a7f951ada5f2e66fbea43b5782d03f5d19bf9bf..61755adfceae97bb8b5020f8ca556d79a2a11c7b 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 0000000000000000000000000000000000000000..05014bbdaa8361481c7b47c3ddbb6435f7db8ee8 --- /dev/null +++ b/nodes/solid/solid_select.py @@ -0,0 +1,481 @@ +# 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, 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 +from sverchok.dependencies import FreeCAD + +if FreeCAD is None: + add_dummy('SvSelectSolidNode', 'Select Solid Elements', 'FreeCAD') +else: + import FreeCAD + import Part + from FreeCAD import Base + +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 = 'UV_SYNC_SELECT' + 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), + ('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', '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", + 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'} + 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', 'SOLID_INSIDE'} + self.inputs['Precision'].hide_safe = self.element_type == 'VERTS' or self.criteria_type in {'SOLID_DISTANCE'} + + 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 - 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) + + 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) + + 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'}: + if self.element_type == 'EDGES': + text = "1. Partially selected edges" + else: + text = "1. Partially selected faces" + layout.prop(self, 'include_partial', text=text) + + if self.element_type == 'VERTS': + text = "2. Partially selected edges, faces" + layout.prop(self, 'include_partial_other', text=text) + elif self.element_type == 'EDGES': + text = "2. 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') + + 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) + + 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) + + 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 + + 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): + 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() + + 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 + + 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): + 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(normal): + return direction.dot(normal) + + 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() + + 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) + + 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 + + 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): + 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) + 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] + 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': + 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) + 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] + vertex_mask = topo.get_vertices_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': + 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) + 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) + 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] + 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() + 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: + tool_s = [[None]] + 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() + + 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) + 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 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, 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) + + 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) + 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 7123a68d53253362a0fdfe989fed723e25b79cde..3a95772adc81970a763f8232fe980163a1090fcf 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,201 @@ 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 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))] + + 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, 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, 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, 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() + 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 = []