From 4530aec1ff2d0a8586d98df0880fa04f447af5fa Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 22 Sep 2020 22:45:39 +0500 Subject: [PATCH 1/7] Solid Boolean: support nesting level up to 2. --- nodes/solid/solid_boolean.py | 268 ++++++++-------- utils/solid.py | 574 ++++++++++++++++++++--------------- 2 files changed, 451 insertions(+), 391 deletions(-) diff --git a/nodes/solid/solid_boolean.py b/nodes/solid/solid_boolean.py index db652989c..1c7f96837 100644 --- a/nodes/solid/solid_boolean.py +++ b/nodes/solid/solid_boolean.py @@ -1,156 +1,138 @@ - +# 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 bpy +from bpy.props import BoolProperty, FloatProperty, EnumProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat as mlr, zip_long_repeat, ensure_nesting_level, get_data_nesting_level from sverchok.dependencies import FreeCAD from sverchok.utils.dummy_nodes import add_dummy if FreeCAD is None: add_dummy('SvSolidBooleanNode', 'Solid Boolean', 'FreeCAD') else: - import bpy - from bpy.props import BoolProperty, FloatProperty, EnumProperty - from sverchok.node_tree import SverchCustomTreeNode - from sverchok.data_structure import updateNode, match_long_repeat as mlr import Part - class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): - """ - Triggers: Union, Diff, Intersect - Tooltip: Perform Boolean Operations on Solids - """ - bl_idname = 'SvSolidBooleanNode' - bl_label = 'Solid Boolean' - bl_icon = 'OUTLINER_OB_EMPTY' - sv_icon = 'SV_SOLID_BOOLEAN' - solid_catergory = "Operators" - - - precision: FloatProperty( - name="Length", - default=0.1, - precision=4, - update=updateNode) - mode_options = [ - ("ITX", "Intersect", "", 0), - ("UNION", "Union", "", 1), - ("DIFF", "Difference", "", 2) - ] - - selected_mode: EnumProperty( - name='Operation', - items=mode_options, - description="basic booleans using solids", - default="ITX", - update=updateNode) - - def update_mode(self, context): - self.inputs['Solid A'].hide_safe = self.nest_objs - self.inputs['Solid B'].hide_safe = self.nest_objs - self.inputs['Solids'].hide_safe = not self.nest_objs - updateNode(self, context) - - nest_objs: BoolProperty( - name="Accumulate nested", - description="bool first two solids, then applies rest to result one by one", - default=False, - update=update_mode) - refine_solid: BoolProperty( - name="Refine Solid", - description="Removes redundant edges (may slow the process)", - default=True, - update=updateNode) - - def draw_buttons(self, context, layout): - layout.prop(self, "selected_mode", toggle=True) - layout.prop(self, "nest_objs", toggle=True) - if self.selected_mode == 'UNION': - layout.prop(self, "refine_solid") - - def sv_init(self, context): - self.inputs.new('SvSolidSocket', "Solid A") - self.inputs.new('SvSolidSocket', "Solid B") - self.inputs.new('SvSolidSocket', "Solids") - self.inputs['Solids'].hide_safe = True - - - self.outputs.new('SvSolidSocket', "Solid") - - - - def single_union(self): - solids_a = self.inputs[0].sv_get() - solids_b = self.inputs[1].sv_get() - solids = [] - for solid_a, solid_b in zip(*mlr([solids_a, solids_b])): - solid_out = solid_a.fuse(solid_b) - if self.refine_solid: - solid_out = solid_out.removeSplitter() - solids.append(solid_out) - self.outputs[0].sv_set(solids) - - def multi_union(self): - solids = self.inputs[2].sv_get() - base = solids[0].copy() - base = base.fuse(solids[1:]) -# for s in solids[1:]: -# base = base.fuse(s) - if self.refine_solid: - base = base.removeSplitter() - self.outputs[0].sv_set([base]) - - def single_intersect(self): - solids_a = self.inputs[0].sv_get() - solids_b = self.inputs[1].sv_get() - solids = [] - for solid_a, solid_b in zip(*mlr([solids_a, solids_b])): - solids.append(solid_a.common(solid_b)) - self.outputs[0].sv_set(solids) - - def multi_intersect(self): - solids = self.inputs[2].sv_get() - base = solids[0].copy() - base = base.common(solids[1:]) -# for s in solids[1:]: -# base = base.common(s) - self.outputs[0].sv_set([base]) - - def single_difference(self): - solids_a = self.inputs[0].sv_get() - solids_b = self.inputs[1].sv_get() - solids = [] - for solid_a, solid_b in zip(*mlr([solids_a, solids_b])): - shape = solid_a.cut(solid_b) - - solids.append(shape) - self.outputs[0].sv_set(solids) - - def multi_difference(self): - solids = self.inputs[2].sv_get() - base = solids[0].copy() - base = base.cut(solids[1:]) -# for s in solids[1:]: -# base = base.cut(s) - - self.outputs[0].sv_set([base]) - - def process(self): - if not any(socket.is_linked for socket in self.outputs): - return - - if self.selected_mode == 'UNION': - if self.nest_objs: - self.multi_union() - else: - self.single_union() - elif self.selected_mode == 'ITX': - if self.nest_objs: - self.multi_intersect() - else: - self.single_intersect() - else: - if self.nest_objs: - self.multi_difference() +class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Union, Diff, Intersect + Tooltip: Perform Boolean Operations on Solids + """ + bl_idname = 'SvSolidBooleanNode' + bl_label = 'Solid Boolean' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SOLID_BOOLEAN' + solid_catergory = "Operators" + + precision: FloatProperty( + name="Length", + default=0.1, + precision=4, + update=updateNode) + + mode_options = [ + ("ITX", "Intersect", "", 0), + ("UNION", "Union", "", 1), + ("DIFF", "Difference", "", 2) + ] + + selected_mode: EnumProperty( + name='Operation', + items=mode_options, + description="basic booleans using solids", + default="ITX", + update=updateNode) + + def update_mode(self, context): + self.inputs['Solid A'].hide_safe = self.nest_objs + self.inputs['Solid B'].hide_safe = self.nest_objs + self.inputs['Solids'].hide_safe = not self.nest_objs + updateNode(self, context) + + nest_objs: BoolProperty( + name="Accumulate nested", + description="bool first two solids, then applies rest to result one by one", + default=False, + update=update_mode) + + refine_solid: BoolProperty( + name="Refine Solid", + description="Removes redundant edges (may slow the process)", + default=True, + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "selected_mode", toggle=True) + layout.prop(self, "nest_objs", toggle=True) + if self.selected_mode == 'UNION': + layout.prop(self, "refine_solid") + + def sv_init(self, context): + self.inputs.new('SvSolidSocket', "Solid A") + self.inputs.new('SvSolidSocket', "Solid B") + self.inputs.new('SvSolidSocket', "Solids") + self.inputs['Solids'].hide_safe = True + + self.outputs.new('SvSolidSocket', "Solid") + + def make_solid(self, solids): + do_refine = self.refine_solid and self.selected_mode in {'UNION'} + base = solids[0].copy() + rest = solids[1:] + if self.selected_mode == 'UNION': + solid = base.fuse(rest) + elif sel.selected_mode == 'ITX': + solid = base.common(rest) + elif self.selected_mode == 'DIFF': + solid = base.cut(rest) + else: + raise Exception("Unknown mode") + if do_refine: + solid = solid.removeSplitter() + return solid + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + solids_out = [] + if self.nest_objs: + solids_in = self.inputs['Solids'].sv_get() + #level = get_data_nesting_level(solids_in, data_types=(Part.Shape,)) + solids_in = ensure_nesting_level(solids_in, 2, data_types=(Part.Shape,)) + + for solids in solids_in: + solid = self.make_solid(solids) + solids_out.append(solid) + + self.outputs['Solid'].sv_set(solids_out) + + else: + solids_a_in = self.inputs['Solid A'].sv_get() + solids_b_in = self.inputs['Solid B'].sv_get() + level_a = get_data_nesting_level(solids_a_in, data_types=(Part.Shape,)) + level_b = get_data_nesting_level(solids_b_in, data_types=(Part.Shape,)) + solids_a_in = ensure_nesting_level(solids_a_in, 2, data_types=(Part.Shape,)) + solids_b_in = ensure_nesting_level(solids_b_in, 2, data_types=(Part.Shape,)) + level = max(level_a, level_b) + + solids_out = [] + for params in zip_long_repeat(solids_a_in, solids_b_in): + new_solids = [] + for solid_a, solid_b in zip_long_repeat(*params): + solid = self.make_solid([solid_a, solid_b]) + new_solids.append(solid) + if level == 1: + solids_out.extend(new_solids) else: - self.single_difference() + solids_out.append(new_solids) + self.outputs['Solid'].sv_set(solids_out) def register(): if FreeCAD is not None: diff --git a/utils/solid.py b/utils/solid.py index 709fa822d..04f858bb1 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -22,267 +22,345 @@ 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 +class SvSolidTopology(object): + class Item(object): + def __init__(self, item): + self.item = item - def __hash__(self): - return self.item.hashCode() + 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 __eq__(self, other): + return self.item.isSame(other.item) 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 + 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 edge in self.solid.Edges: - test = condition(np.array(self._points_by_edge[SvSolidTopology.Item(edge)])) - mask.append(check(test)) + 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_location_mask(self, condition, include_partial): - # condition is vectorized - check = any if include_partial else all + 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(face) in good for face in self.solid.Faces] + else: + edges = set([SvSolidTopology.Item(e) for e in edges]) mask = [] for face in self.solid.Faces: - test = condition(np.array(self._points_by_face[SvSolidTopology.Item(face)])) - mask.append(check(test)) + ok = all(SvSolidTopology.Item(e) in edges for e in face.Edges) + mask.append(ok) return mask - def get_faces_by_vertex(self, vertex): - return [i.item for i in self._faces_by_vertex[SvSolidTopology.Item(vertex)]] + def get_edges_by_vertex(self, vertex): + return [i.item for i in self._edges_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(face) in good for face in self.solid.Faces] - 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): + def get_edges_by_vertices_mask(self, vertices, include_partial=True): + if include_partial: good = set() - for face in faces: - new = set([SvSolidTopology.Item(e) for e in face.Edges]) + 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_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 = [] - for solid, precision in zip(*mlr([solids, precisions])): - rawdata = solid.tessellate(precision) - b_verts = [] - b_faces = [] - for v in rawdata[0]: - b_verts.append((v.x, v.y, v.z)) - for f in rawdata[1]: - b_faces.append(f) - verts.append(b_verts) - faces.append(b_faces) - - return verts, faces - - def standard_mesher(solids, surface_deviation, angle_deviation, relative_surface_deviation): - verts = [] - faces = [] - for solid, s_dev, ang_dev in zip(*mlr([solids, surface_deviation, angle_deviation])): - mesh = MeshPart.meshFromShape( - Shape=solid, - LinearDeflection=s_dev, - AngularDeflection=math.radians(ang_dev), - Relative=relative_surface_deviation) - - verts.append([v[:] for v in mesh.Topology[0]]) - faces.append(mesh.Topology[1]) - - return verts, faces - - def mefisto_mesher(solids, max_edge_length): - - verts = [] - faces = [] - for solid, max_edge in zip(*mlr([solids, max_edge_length])): - mesh = MeshPart.meshFromShape( - Shape=solid, - MaxLength=max_edge - ) - - verts.append([v[:] for v in mesh.Topology[0]]) - faces.append(mesh.Topology[1]) - - return verts, faces - - - def svmesh_to_solid(verts, faces, precision, remove_splitter=True): - """ - input: - verts: list of 3element iterables, [vector, vector...] - faces: list of lists of face indices - precision: a conversion factor defined in makeShapeFromMesh (FreeCAD) - remove_splitter: default True, removes duplicate geometry (edges) - output: - a FreeCAD solid - - """ - tri_faces = ensure_triangles(verts, faces, True) - faces_t = [[verts[c] for c in f] for f in tri_faces] - mesh = Mesh.Mesh(faces_t) - shape = Part.Shape() - shape.makeShapeFromMesh(mesh.Topology, precision) - - if remove_splitter: - # may slow it down, or be totally necessary - shape = shape.removeSplitter() - - return Part.makeSolid(shape) + 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))] + +class SvGeneralFuse(object): + def __init__(self, solids): + self.solids = solids + self.result, self.map = solids[0].generalFuse(solids[1:]) + self._per_source = defaultdict(set) + self._per_source_idx = defaultdict(set) + for i, (source, items) in enumerate(zip(solids, self.map)): + items = set(SvSolidTopology.Item(i) for i in items) + key = SvSolidTopology.Item(source) + self._per_source[key] = items + self._per_source_idx[i] = items + + self._edge_sources = defaultdict(list) + for part in self.result.Solids: + for edge in part.Edges: + self._edge_sources[SvSolidTopology.Item(edge)].append(part) + + def get_all_parts(self): + return self.result.Solids + + def get_union_all(self): + solids = self.result.Solids + return solids[0].fuse(solids[1:]) + + def get_intersect_all(self): + result = None + for source, parts in self._per_source.items(): + if result is None: + result = parts + else: + result.intersection_update(parts) + if not result: + return None + elif len(result) == 1: + return list(result)[0] + else: + solids = list(result) + return solids[0].fuse(solids[1:]) + + def get_edge_sources(self, edge): + return self._edge_sources[SvSolidTopology.Item(edge)] + + def get_by_source(self, solid): + return self._per_source[SvSolidTopology.Item(solid)] + + def get_by_source_idx(self, idx): + return self._per_source_idx[idx] + + def get_intersection(self, solid_a, solid_b): + result_a = self._per_source[SvSolidTopology.Item(solid_a)] + result_b = self._per_source[SvSolidTopology.Item(solid_b)] + return result_a.intersection(result_b) + + def get_intersection_by_idx(self, idx_a, idx_b): + result_a = self._per_source_idx[idx_a] + result_b = self._per_source_idx[idx_b] + return result_a.intersection(result_b) + + def get_difference(self, solid_a, solid_b): + result_a = self._per_source[SvSolidTopology.Item(solid_a)] + result_b = self._per_source[SvSolidTopology.Item(solid_b)] + return result_a.difference(result_b) + + def get_clean_part(self, solid): + item = SvSolidTopology.Item(solid) + result = self._per_source[item].copy() + for source, results in self._per_source.items(): + if source != item: + result.difference_update(results) + return result + + def get_clean_part_by_idx(self, idx): + result = self._per_source_idx[idx].copy() + for source_idx, results in self._per_source_idx.items(): + if source_idx != idx: + result.difference_update(results) + return result + +def basic_mesher(solids, precisions): + verts = [] + faces = [] + for solid, precision in zip(*mlr([solids, precisions])): + rawdata = solid.tessellate(precision) + b_verts = [] + b_faces = [] + for v in rawdata[0]: + b_verts.append((v.x, v.y, v.z)) + for f in rawdata[1]: + b_faces.append(f) + verts.append(b_verts) + faces.append(b_faces) + + return verts, faces + +def standard_mesher(solids, surface_deviation, angle_deviation, relative_surface_deviation): + verts = [] + faces = [] + for solid, s_dev, ang_dev in zip(*mlr([solids, surface_deviation, angle_deviation])): + mesh = MeshPart.meshFromShape( + Shape=solid, + LinearDeflection=s_dev, + AngularDeflection=math.radians(ang_dev), + Relative=relative_surface_deviation) + + verts.append([v[:] for v in mesh.Topology[0]]) + faces.append(mesh.Topology[1]) + + return verts, faces + +def mefisto_mesher(solids, max_edge_length): + + verts = [] + faces = [] + for solid, max_edge in zip(*mlr([solids, max_edge_length])): + mesh = MeshPart.meshFromShape( + Shape=solid, + MaxLength=max_edge + ) + + verts.append([v[:] for v in mesh.Topology[0]]) + faces.append(mesh.Topology[1]) + + return verts, faces + + +def svmesh_to_solid(verts, faces, precision, remove_splitter=True): + """ + input: + verts: list of 3element iterables, [vector, vector...] + faces: list of lists of face indices + precision: a conversion factor defined in makeShapeFromMesh (FreeCAD) + remove_splitter: default True, removes duplicate geometry (edges) + output: + a FreeCAD solid + + """ + tri_faces = ensure_triangles(verts, faces, True) + faces_t = [[verts[c] for c in f] for f in tri_faces] + mesh = Mesh.Mesh(faces_t) + shape = Part.Shape() + shape.makeShapeFromMesh(mesh.Topology, precision) + + if remove_splitter: + # may slow it down, or be totally necessary + shape = shape.removeSplitter() + + return Part.makeSolid(shape) -- GitLab From 42502d045b411796ca51340c325984db304be606 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Sep 2020 00:10:52 +0500 Subject: [PATCH 2/7] "EdgeMask" output. --- nodes/solid/solid_boolean.py | 73 ++++++++++++++++++++++++++++----- utils/solid.py | 78 ++++++++++++++++++++++++++++-------- 2 files changed, 124 insertions(+), 27 deletions(-) diff --git a/nodes/solid/solid_boolean.py b/nodes/solid/solid_boolean.py index 1c7f96837..c3f41c852 100644 --- a/nodes/solid/solid_boolean.py +++ b/nodes/solid/solid_boolean.py @@ -8,7 +8,7 @@ import bpy from bpy.props import BoolProperty, FloatProperty, EnumProperty -from sverchok.node_tree import SverchCustomTreeNode +from sverchok.node_tree import SverchCustomTreeNode, throttled from sverchok.data_structure import updateNode, match_long_repeat as mlr, zip_long_repeat, ensure_nesting_level, get_data_nesting_level from sverchok.dependencies import FreeCAD from sverchok.utils.dummy_nodes import add_dummy @@ -16,6 +16,8 @@ from sverchok.utils.dummy_nodes import add_dummy if FreeCAD is None: add_dummy('SvSolidBooleanNode', 'Solid Boolean', 'FreeCAD') else: + from sverchok.utils.solid import SvGeneralFuse + import Part class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): @@ -48,11 +50,12 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): default="ITX", update=updateNode) + @throttled def update_mode(self, context): self.inputs['Solid A'].hide_safe = self.nest_objs self.inputs['Solid B'].hide_safe = self.nest_objs self.inputs['Solids'].hide_safe = not self.nest_objs - updateNode(self, context) + self.outputs['EdgeMask'].hide_safe = not self.generate_masks nest_objs: BoolProperty( name="Accumulate nested", @@ -66,8 +69,15 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): default=True, update=updateNode) + generate_masks : BoolProperty( + name = "Generate Masks", + description = "Calculate masks so it will be possible to know which edges / faces are new", + default = False, + update = update_mode) + def draw_buttons(self, context, layout): layout.prop(self, "selected_mode", toggle=True) + layout.prop(self, 'generate_masks', toggle=True) layout.prop(self, "nest_objs", toggle=True) if self.selected_mode == 'UNION': layout.prop(self, "refine_solid") @@ -76,41 +86,77 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvSolidSocket', "Solid A") self.inputs.new('SvSolidSocket', "Solid B") self.inputs.new('SvSolidSocket', "Solids") - self.inputs['Solids'].hide_safe = True self.outputs.new('SvSolidSocket', "Solid") + self.outputs.new('SvStringsSocket', "EdgeMask") + + self.update_mode(context) def make_solid(self, solids): - do_refine = self.refine_solid and self.selected_mode in {'UNION'} + if self.generate_masks: + solid, mask = self.make_solid_general(solids) + else: + solid = self.make_solid_simple(solids) + mask = [] + #do_refine = self.refine_solid and self.selected_mode in {'UNION'} + #if do_refine: + # solid = solid.removeSplitter() + return solid, mask + + def make_solid_simple(self, solids): base = solids[0].copy() rest = solids[1:] if self.selected_mode == 'UNION': solid = base.fuse(rest) - elif sel.selected_mode == 'ITX': + elif self.selected_mode == 'ITX': solid = base.common(rest) elif self.selected_mode == 'DIFF': solid = base.cut(rest) else: raise Exception("Unknown mode") + do_refine = self.refine_solid and self.selected_mode in {'UNION'} if do_refine: solid = solid.removeSplitter() return solid + def make_solid_general(self, solids): + do_refine = self.refine_solid and self.selected_mode in {'UNION'} + + fused = SvGeneralFuse(solids) + if self.selected_mode == 'UNION': + solid = fused.get_union_all(refine=do_refine) + elif self.selected_mode == 'ITX': + solid = fused.get_intersect_all(refine=do_refine) + elif self.selected_mode == 'DIFF': + solid = fused.get_clean_part_by_idx(0, refine=do_refine) + else: + raise Exception("Unknown mode") + + mask = [] + if solid is not None: + for i, edge in enumerate(solid.Edges): + srcs = fused.get_edge_source_idxs(edge) + print(f"E#{i} => {srcs}") + is_new = len(srcs) > 1 + mask.append(is_new) + + return solid, mask + def process(self): if not any(socket.is_linked for socket in self.outputs): return solids_out = [] + edge_masks_out = [] if self.nest_objs: solids_in = self.inputs['Solids'].sv_get() #level = get_data_nesting_level(solids_in, data_types=(Part.Shape,)) solids_in = ensure_nesting_level(solids_in, 2, data_types=(Part.Shape,)) for solids in solids_in: - solid = self.make_solid(solids) + solid, edge_mask = self.make_solid(solids) solids_out.append(solid) - - self.outputs['Solid'].sv_set(solids_out) + edge_masks_out.append(edge_mask) else: solids_a_in = self.inputs['Solid A'].sv_get() @@ -121,18 +167,23 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): solids_b_in = ensure_nesting_level(solids_b_in, 2, data_types=(Part.Shape,)) level = max(level_a, level_b) - solids_out = [] for params in zip_long_repeat(solids_a_in, solids_b_in): new_solids = [] + new_edge_masks = [] for solid_a, solid_b in zip_long_repeat(*params): - solid = self.make_solid([solid_a, solid_b]) + solid, edge_mask = self.make_solid([solid_a, solid_b]) new_solids.append(solid) + new_edge_masks.append(edge_mask) if level == 1: solids_out.extend(new_solids) + edge_masks_out.extend(new_edge_masks) else: solids_out.append(new_solids) + edge_masks_out.append(new_edge_masks) - self.outputs['Solid'].sv_set(solids_out) + self.outputs['Solid'].sv_set(solids_out) + if 'EdgeMask' in self.outputs: + self.outputs['EdgeMask'].sv_set(edge_masks_out) def register(): if FreeCAD is not None: diff --git a/utils/solid.py b/utils/solid.py index 04f858bb1..0b0f2b6ad 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -223,25 +223,55 @@ class SvGeneralFuse(object): self.result, self.map = solids[0].generalFuse(solids[1:]) self._per_source = defaultdict(set) self._per_source_idx = defaultdict(set) - for i, (source, items) in enumerate(zip(solids, self.map)): - items = set(SvSolidTopology.Item(i) for i in items) - key = SvSolidTopology.Item(source) - self._per_source[key] = items - self._per_source_idx[i] = items + self._sources_by_part = defaultdict(set) + self._source_idxs_by_part = defaultdict(set) + for src_idx, (source, parts) in enumerate(zip(solids, self.map)): + items = set(SvSolidTopology.Item(i) for i in parts) + + src_key = SvSolidTopology.Item(source) + self._per_source[src_key] = items + self._per_source_idx[src_idx] = items + + for item in items: + self._sources_by_part[item].add(src_key) + self._source_idxs_by_part[item].add(src_idx) - self._edge_sources = defaultdict(list) + self._edge_indirect_source_idxs = defaultdict(set) + self._edge_indirect_sources = defaultdict(set) for part in self.result.Solids: + item = SvSolidTopology.Item(part) + sources = self._sources_by_part[item] + src_idxs = self._source_idxs_by_part[item] for edge in part.Edges: - self._edge_sources[SvSolidTopology.Item(edge)].append(part) + edge_item = SvSolidTopology.Item(edge) + self._edge_indirect_sources[edge_item].update(sources) + self._edge_indirect_source_idxs[edge_item].update(src_idxs) + +# self._edge_direct_source_idxs = defaultdict(set) +# self._edge_direct_sources = defaultdict(set) +# for part in self.result.Solids: +# item = SvSolidTopology.Item(part) +# for edge in part.Edges: +# edge_item = SvSolidTopology.Item(edge) +# indirect_sources = self._edge_indirect_sources[edge_item] +# indirect_source_idxs = self._edge_indirect_source_idxs[edge_item] +# for src_idx, indirect_source in zip(indirect_source_idxs, indirect_sources): +# src_edges = set(SvSolidTopology.Item(e) for e in indirect_source.item.Edges) +# if edge_item in src_edges: +# self._edge_direct_sources[edge_item].add(indirect_source) +# self._edge_direct_source_idxs[edge_item].add(src_idx) def get_all_parts(self): return self.result.Solids - def get_union_all(self): + def get_union_all(self, refine=False): solids = self.result.Solids - return solids[0].fuse(solids[1:]) + solid = solids[0].fuse(solids[1:]) + if refine: + solid = solid.removeSplitter() + return solid - def get_intersect_all(self): + def get_intersect_all(self, refine=False): result = None for source, parts in self._per_source.items(): if result is None: @@ -251,13 +281,19 @@ class SvGeneralFuse(object): if not result: return None elif len(result) == 1: - return list(result)[0] + return list(result)[0].item else: - solids = list(result) - return solids[0].fuse(solids[1:]) + solids = [p.item for p in result] + solid = solids[0].fuse(solids[1:]) + if refine: + solid = solid.removeSplitter() + return solid def get_edge_sources(self, edge): - return self._edge_sources[SvSolidTopology.Item(edge)] + return self._edge_indirect_sources[SvSolidTopology.Item(edge)] + + def get_edge_source_idxs(self, edge): + return self._edge_indirect_source_idxs[SvSolidTopology.Item(edge)] def get_by_source(self, solid): return self._per_source[SvSolidTopology.Item(solid)] @@ -288,12 +324,22 @@ class SvGeneralFuse(object): result.difference_update(results) return result - def get_clean_part_by_idx(self, idx): + def get_clean_part_by_idx(self, idx, refine=False): result = self._per_source_idx[idx].copy() for source_idx, results in self._per_source_idx.items(): if source_idx != idx: result.difference_update(results) - return result + + parts = [part.item for part in result] + if not parts: + solid = None + elif len(parts) == 1: + solid = parts[0] + else: + solid = parts[0].fuse(parts[1:]) + if do_refine: + solid = solid.removeSplitter() + return solid def basic_mesher(solids, precisions): verts = [] -- GitLab From c3328d1e3263c5c0f3b7486529310b1879572e3c Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Sep 2020 00:46:05 +0500 Subject: [PATCH 3/7] "EdgeSources" output support. --- nodes/solid/solid_boolean.py | 49 ++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/nodes/solid/solid_boolean.py b/nodes/solid/solid_boolean.py index c3f41c852..d24ba86b8 100644 --- a/nodes/solid/solid_boolean.py +++ b/nodes/solid/solid_boolean.py @@ -20,6 +20,16 @@ else: import Part +class BoolResult(object): + def __init__(self, solid, edge_mask=None, edge_map=None): + self.solid = solid + if edge_mask is None: + edge_mask = [] + self.edge_mask = edge_mask + if edge_map is None: + edge_map = [] + self.edge_map = edge_map + class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): """ Triggers: Union, Diff, Intersect @@ -56,6 +66,7 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): self.inputs['Solid B'].hide_safe = self.nest_objs self.inputs['Solids'].hide_safe = not self.nest_objs self.outputs['EdgeMask'].hide_safe = not self.generate_masks + self.outputs['EdgeSources'].hide_safe = not self.generate_masks nest_objs: BoolProperty( name="Accumulate nested", @@ -89,19 +100,16 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): self.outputs.new('SvSolidSocket', "Solid") self.outputs.new('SvStringsSocket', "EdgeMask") + self.outputs.new('SvStringsSocket', "EdgeSources") self.update_mode(context) def make_solid(self, solids): if self.generate_masks: - solid, mask = self.make_solid_general(solids) + return self.make_solid_general(solids) else: solid = self.make_solid_simple(solids) - mask = [] - #do_refine = self.refine_solid and self.selected_mode in {'UNION'} - #if do_refine: - # solid = solid.removeSplitter() - return solid, mask + return BoolResult(solid) def make_solid_simple(self, solids): base = solids[0].copy() @@ -132,15 +140,16 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): else: raise Exception("Unknown mode") - mask = [] + edge_mask = [] + edge_map = [] if solid is not None: for i, edge in enumerate(solid.Edges): srcs = fused.get_edge_source_idxs(edge) - print(f"E#{i} => {srcs}") + edge_map.append(list(srcs)) is_new = len(srcs) > 1 - mask.append(is_new) + edge_mask.append(is_new) - return solid, mask + return BoolResult(solid, edge_mask, edge_map) def process(self): if not any(socket.is_linked for socket in self.outputs): @@ -148,15 +157,17 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): solids_out = [] edge_masks_out = [] + edge_srcs_out = [] if self.nest_objs: solids_in = self.inputs['Solids'].sv_get() #level = get_data_nesting_level(solids_in, data_types=(Part.Shape,)) solids_in = ensure_nesting_level(solids_in, 2, data_types=(Part.Shape,)) for solids in solids_in: - solid, edge_mask = self.make_solid(solids) - solids_out.append(solid) - edge_masks_out.append(edge_mask) + result = self.make_solid(solids) + solids_out.append(result.solid) + edge_masks_out.append(result.edge_mask) + edge_srcs_out.append(result.edge_map) else: solids_a_in = self.inputs['Solid A'].sv_get() @@ -170,20 +181,26 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): for params in zip_long_repeat(solids_a_in, solids_b_in): new_solids = [] new_edge_masks = [] + new_edge_srcs = [] for solid_a, solid_b in zip_long_repeat(*params): - solid, edge_mask = self.make_solid([solid_a, solid_b]) - new_solids.append(solid) - new_edge_masks.append(edge_mask) + result = self.make_solid([solid_a, solid_b]) + new_solids.append(result.solid) + new_edge_masks.append(result.edge_mask) + new_edge_srcs.append(result.edge_map) if level == 1: solids_out.extend(new_solids) edge_masks_out.extend(new_edge_masks) + edge_srcs_out.extend(new_edge_srcs) else: solids_out.append(new_solids) edge_masks_out.append(new_edge_masks) + edge_srcs_out.append(new_edge_srcs) self.outputs['Solid'].sv_set(solids_out) if 'EdgeMask' in self.outputs: self.outputs['EdgeMask'].sv_set(edge_masks_out) + if 'EdgeSources' in self.outputs: + self.outputs['EdgeSources'].sv_set(edge_srcs_out) def register(): if FreeCAD is not None: -- GitLab From 5e1f708ab552d2682d6391e5a048c73c8a5649f9 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Sep 2020 01:14:15 +0500 Subject: [PATCH 4/7] Face masks outputs. --- nodes/solid/solid_boolean.py | 49 ++++++++++++++++++++++++++++++------ utils/solid.py | 14 +++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/nodes/solid/solid_boolean.py b/nodes/solid/solid_boolean.py index d24ba86b8..ca309c3ea 100644 --- a/nodes/solid/solid_boolean.py +++ b/nodes/solid/solid_boolean.py @@ -21,7 +21,7 @@ else: import Part class BoolResult(object): - def __init__(self, solid, edge_mask=None, edge_map=None): + def __init__(self, solid, edge_mask=None, edge_map=None, face_mask=None, face_map=None): self.solid = solid if edge_mask is None: edge_mask = [] @@ -29,6 +29,12 @@ class BoolResult(object): if edge_map is None: edge_map = [] self.edge_map = edge_map + if face_mask is None: + face_mask = [] + self.face_mask = face_mask + if face_map is None: + face_map = [] + self.face_map = face_map class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): """ @@ -65,8 +71,10 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): self.inputs['Solid A'].hide_safe = self.nest_objs self.inputs['Solid B'].hide_safe = self.nest_objs self.inputs['Solids'].hide_safe = not self.nest_objs - self.outputs['EdgeMask'].hide_safe = not self.generate_masks + self.outputs['EdgesMask'].hide_safe = not self.generate_masks self.outputs['EdgeSources'].hide_safe = not self.generate_masks + self.outputs['FacesMask'].hide_safe = not self.generate_masks + self.outputs['FaceSources'].hide_safe = not self.generate_masks nest_objs: BoolProperty( name="Accumulate nested", @@ -99,8 +107,10 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvSolidSocket', "Solids") self.outputs.new('SvSolidSocket', "Solid") - self.outputs.new('SvStringsSocket', "EdgeMask") + self.outputs.new('SvStringsSocket', "EdgesMask") self.outputs.new('SvStringsSocket', "EdgeSources") + self.outputs.new('SvStringsSocket', "FacesMask") + self.outputs.new('SvStringsSocket', "FaceSources") self.update_mode(context) @@ -142,14 +152,23 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): edge_mask = [] edge_map = [] + face_mask = [] + face_map = [] + if solid is not None: - for i, edge in enumerate(solid.Edges): + for edge in solid.Edges: srcs = fused.get_edge_source_idxs(edge) edge_map.append(list(srcs)) is_new = len(srcs) > 1 edge_mask.append(is_new) - return BoolResult(solid, edge_mask, edge_map) + for face in solid.Faces: + srcs = fused.get_face_source_idxs(face) + face_map.append(list(srcs)) + is_new = len(srcs) > 1 + face_mask.append(is_new) + + return BoolResult(solid, edge_mask, edge_map, face_mask, face_map) def process(self): if not any(socket.is_linked for socket in self.outputs): @@ -158,6 +177,8 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): solids_out = [] edge_masks_out = [] edge_srcs_out = [] + face_masks_out = [] + face_srcs_out = [] if self.nest_objs: solids_in = self.inputs['Solids'].sv_get() #level = get_data_nesting_level(solids_in, data_types=(Part.Shape,)) @@ -168,6 +189,8 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): solids_out.append(result.solid) edge_masks_out.append(result.edge_mask) edge_srcs_out.append(result.edge_map) + face_masks_out.append(result.face_mask) + face_srcs_out.append(result.face_map) else: solids_a_in = self.inputs['Solid A'].sv_get() @@ -182,25 +205,37 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): new_solids = [] new_edge_masks = [] new_edge_srcs = [] + new_face_masks = [] + new_face_srcs = [] for solid_a, solid_b in zip_long_repeat(*params): result = self.make_solid([solid_a, solid_b]) new_solids.append(result.solid) new_edge_masks.append(result.edge_mask) new_edge_srcs.append(result.edge_map) + new_face_masks.append(result.face_mask) + new_face_srcs.append(result.face_map) if level == 1: solids_out.extend(new_solids) edge_masks_out.extend(new_edge_masks) edge_srcs_out.extend(new_edge_srcs) + face_masks_out.extend(new_face_masks) + face_srcs_out.extend(new_face_srcs) else: solids_out.append(new_solids) edge_masks_out.append(new_edge_masks) edge_srcs_out.append(new_edge_srcs) + face_masks_out.append(new_face_masks) + face_srcs_out.append(new_face_srcs) self.outputs['Solid'].sv_set(solids_out) - if 'EdgeMask' in self.outputs: - self.outputs['EdgeMask'].sv_set(edge_masks_out) + if 'EdgesMask' in self.outputs: + self.outputs['EdgesMask'].sv_set(edge_masks_out) if 'EdgeSources' in self.outputs: self.outputs['EdgeSources'].sv_set(edge_srcs_out) + if 'FacesMask' in self.outputs: + self.outputs['FacesMask'].sv_set(face_masks_out) + if 'FaceSources' in self.outputs: + self.outputs['FaceSources'].sv_set(face_srcs_out) def register(): if FreeCAD is not None: diff --git a/utils/solid.py b/utils/solid.py index 0b0f2b6ad..88f486c80 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -238,15 +238,23 @@ class SvGeneralFuse(object): self._edge_indirect_source_idxs = defaultdict(set) self._edge_indirect_sources = defaultdict(set) + self._face_indirect_source_idxs = defaultdict(set) + self._face_indirect_sources = defaultdict(set) for part in self.result.Solids: item = SvSolidTopology.Item(part) sources = self._sources_by_part[item] src_idxs = self._source_idxs_by_part[item] + for edge in part.Edges: edge_item = SvSolidTopology.Item(edge) self._edge_indirect_sources[edge_item].update(sources) self._edge_indirect_source_idxs[edge_item].update(src_idxs) + for face in part.Faces: + face_item = SvSolidTopology.Item(face) + self._face_indirect_source_idxs[face_item].update(src_idxs) + self._face_indirect_sources[face_item].update(sources) + # self._edge_direct_source_idxs = defaultdict(set) # self._edge_direct_sources = defaultdict(set) # for part in self.result.Solids: @@ -294,6 +302,12 @@ class SvGeneralFuse(object): def get_edge_source_idxs(self, edge): return self._edge_indirect_source_idxs[SvSolidTopology.Item(edge)] + + def get_face_sources(self, face): + return self._face_indirect_sources[SvSolidTopology.Item(face)] + + def get_face_source_idxs(self, face): + return self._face_indirect_source_idxs[SvSolidTopology.Item(face)] def get_by_source(self, solid): return self._per_source[SvSolidTopology.Item(solid)] -- GitLab From 2bdb472b6724042d776dbfb3415b9fd2e314db64 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Sep 2020 01:31:46 +0500 Subject: [PATCH 5/7] Force all into Solids... --- nodes/solid/solid_boolean.py | 4 ++++ utils/solid.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/nodes/solid/solid_boolean.py b/nodes/solid/solid_boolean.py index ca309c3ea..8a22b0b7b 100644 --- a/nodes/solid/solid_boolean.py +++ b/nodes/solid/solid_boolean.py @@ -140,6 +140,10 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): def make_solid_general(self, solids): do_refine = self.refine_solid and self.selected_mode in {'UNION'} + for i in range(len(solids)): + if not isinstance(solids[i], Part.Solid): + solids[i] = Part.makeSolid(solids[i]) + fused = SvGeneralFuse(solids) if self.selected_mode == 'UNION': solid = fused.get_union_all(refine=do_refine) diff --git a/utils/solid.py b/utils/solid.py index 88f486c80..c77360373 100644 --- a/utils/solid.py +++ b/utils/solid.py @@ -234,6 +234,7 @@ class SvGeneralFuse(object): for item in items: self._sources_by_part[item].add(src_key) + #print(f"{src_idx}: P[{item}] := {src_key}") self._source_idxs_by_part[item].add(src_idx) self._edge_indirect_source_idxs = defaultdict(set) @@ -244,6 +245,7 @@ class SvGeneralFuse(object): item = SvSolidTopology.Item(part) sources = self._sources_by_part[item] src_idxs = self._source_idxs_by_part[item] + #print(f"P? {item} => {src_idxs}") for edge in part.Edges: edge_item = SvSolidTopology.Item(edge) -- GitLab From b71d41bbcb31e4f1f056654c366f685d86111740 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Sep 2020 22:06:16 +0500 Subject: [PATCH 6/7] Update documentation. --- docs/nodes/solid/solid_boolean.rst | 103 +++++++++++++++++++++++++++-- nodes/solid/solid_boolean.py | 6 -- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/docs/nodes/solid/solid_boolean.rst b/docs/nodes/solid/solid_boolean.rst index f4951a3cd..1525c2bf9 100644 --- a/docs/nodes/solid/solid_boolean.rst +++ b/docs/nodes/solid/solid_boolean.rst @@ -1,29 +1,120 @@ Solid Boolean ============= +Dependencies +------------ + +This node requires FreeCAD_ library to work. + +.. _FreeCAD: ../../solids.rst + Functionality ------------- -Offers operations like Solid Union, Difference or Intersection. +This node performs boolean operations on Solid objects: Union ("Fuse"), +Intersection ("Common parts"), Difference ("Cut"). + +The node can operate either on pairs of objects (like "Solid A" plus "Solid +B"), or on lists of objects (fuse several objects at once, or cut several holes +in a body at once). In this "Accumulative" node, the node performs as follows: -The node has two main working modes Regular and Accumulative Nested. +* for Union operation: fuse each list of objects into one body. +* for Intersection operation: take only the part which belongs to **all** + bodies in the list. +* for Difference operation: take the first body in the list, and use all other + bodies as "tools", to cut "holes" in the first body. -In the Regular mode the operation is done between each couple of Solid A and Solid B. +This node can also produce information about which element of the resulting +Solid object came from which source object.Calculating such information can +take time, so this is an optional feature. -In the Accumulative Nested mode the operation is performed along a list of solids between all of them. +Inputs +------ + +This node has the following inputs: + +* **Solid A**. The first Solid object to perform operation on. This input is + available and mandatory only if **Accumulate nested** parameter is not + checked. This input can consume data with nesting level 1 or 2, i.e. lists of + Solids or lists of lists of Solids. +* **Solid B**. The second Solid object to perform operation on. This input is + available and mandatory only if **Accumulate nested** parameter is not + checked. This input can consume data with nesting level 1 or 2, i.e. lists of + Solids or lists of lists of Solids. +* **Solids**. List of Solid objects to perform operation on. This input is + available and mandatory only if **Accumulate nested** parameter is checked. + This input can consume data with nesting level 1 or 2, i.e. lists of Solids + or lists of lists of Solids. The node will make one Solid out of each list of + Solids. Options ------- -The Union mode offers a 'Refine Solid' option to clean unnecessary edges +This node has the following parameters: + +* **Operation**. Boolean operation to perform. The available options are: + **Intersect**, **Union**, **Difference**. The default option is + **Intersect**. +* **Generate Masks**. If checked, the node will generate information about + which element of the resulting Solid came from which solid object. Unchecked + by default. +* **Accumulate nested**. If checked, the node will operate on pairs of objects, + so **Solid A** and **Solid B** inputs will be available. Otherwise, the node + will operate on lists of objects (of arbitrary length), so **Solids** input + will be available. Unchecked by default. +* **Refine Solid**. If checked, the node will refine the generated Solid + object, by removing unnecessary edges. In many cases this is required, but in + cases when this is not required this will only take time. This parameter is + available only if **Operation** parameter is set to **Union**. Checked by + default. Outputs ------- -- Solid +This node has the following outputs: + +* **Solid**. The resulting Solid object. If **Accumulate nested** parameter is + checked, then this output will always contain data of nesting level 1 + (list of Solids). If **Accumulate nested** parameter is not checked, then + this output will contain data of nesting level corresponding to nesting level + in the **Solids A** and **Solids B** inputs, considering the node makes one + Solid out of each pair of Solids. +* **EdgesMask**. Mask for Edges of generated Solid object, which is True for + edges that come from more than one source object. For **Intersect** + operation, this mask will always contain all True. This output is only + available when **Generate Masks** parameter is checked. +* **EdgeSources**. For each Edge of generated Solid object, this output + contains a list of indexes of source objects, from which this edge came. See + more detailed explanation below. This output is only available when + **Generate Masks** parameter is checked. +* **FacesMask**. Mask for Faces of generated Solid object, which is True for + faces that come from more than one source object. For Union operation, this + output contains all False, because all "common" faces are always inside the + body. This output is only available when **Generate Masks** parameter is + checked. +* **FaceSources**. For each Face of generated Solid object, this output + contains a list of indexes of source objects, from which this face came. + This output is only available when **Generate Masks** parameter is checked. + +The following illustrates how **EdgeSources** output is calculated: + +.. image:: https://user-images.githubusercontent.com/284644/94042893-8ccffb00-fde5-11ea-938e-328df1d65d7e.png + +Here we have two cubes, 0 (plugged into Solid A input), and 1 (plugged into +Solid B input). Purple edges came from cube 0, for them EdgeSources output +contains ``[0]``. Orange edges came from cube 1, for them EdgeSources output +contains ``[1]``. Edges marked with cyan came from both cubes, for them +EdgeSources output contains ``[0, 1]``. + +FaceSources output is calculated similarly, but for faces instead of edges. Examples -------- .. image:: https://raw.githubusercontent.com/vicdoval/sverchok/docs_images/images_for_docs/solid/solid_boolean/solid_boolean_blender_sverchok_example.png + +Example of **EdgesMask** output usage: + +.. image:: https://user-images.githubusercontent.com/284644/94038496-f1885700-fddf-11ea-93e6-894aea236ef8.png + diff --git a/nodes/solid/solid_boolean.py b/nodes/solid/solid_boolean.py index 8a22b0b7b..dab859670 100644 --- a/nodes/solid/solid_boolean.py +++ b/nodes/solid/solid_boolean.py @@ -47,12 +47,6 @@ class SvSolidBooleanNode(bpy.types.Node, SverchCustomTreeNode): sv_icon = 'SV_SOLID_BOOLEAN' solid_catergory = "Operators" - precision: FloatProperty( - name="Length", - default=0.1, - precision=4, - update=updateNode) - mode_options = [ ("ITX", "Intersect", "", 0), ("UNION", "Union", "", 1), -- GitLab From 5e894d63c4a9303071e5d1f0047002cc72ad904a Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 23 Sep 2020 22:11:11 +0500 Subject: [PATCH 7/7] Another example. --- docs/nodes/solid/solid_boolean.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/nodes/solid/solid_boolean.rst b/docs/nodes/solid/solid_boolean.rst index 1525c2bf9..6b650ca39 100644 --- a/docs/nodes/solid/solid_boolean.rst +++ b/docs/nodes/solid/solid_boolean.rst @@ -118,3 +118,7 @@ Example of **EdgesMask** output usage: .. image:: https://user-images.githubusercontent.com/284644/94038496-f1885700-fddf-11ea-93e6-894aea236ef8.png +Example of **EdgeSources** output usage. Take five cubes, fuse them, then select only edges that came from intersection of cubes number 1 and 2 (starting from zero), and make fillet for them: + +.. image:: https://user-images.githubusercontent.com/284644/94045977-82176500-fde9-11ea-9c9d-2bce61012280.png + -- GitLab