diff --git a/src/blenderbim/blenderbim/bim/data/assets/default.css b/src/blenderbim/blenderbim/bim/data/assets/default.css index 38a28223be..641962b2da 100644 --- a/src/blenderbim/blenderbim/bim/data/assets/default.css +++ b/src/blenderbim/blenderbim/bim/data/assets/default.css @@ -22,6 +22,7 @@ text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; font-size: 4.13px; } .cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; } .projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } +.surface { stroke: none; fill: #fff; fill-rule: evenodd; } .annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; } diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index 4a7f451076..4c90ec8136 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -1975,79 +1975,15 @@ class CutDecorator: if verts is False: return None, None - if not self.is_intersecting_camera(obj, context.scene.camera): + if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera): DecoratorData.cut_cache[element.id()] = (False, False) return None, None if verts is None: - verts, edges = self.bisect_mesh(obj, context.scene.camera) + verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera) DecoratorData.cut_cache[element.id()] = (verts, edges) return verts, edges - def is_intersecting_camera(self, obj, camera): - # Based on separating axis theorem - plane_co = camera.matrix_world.translation - plane_no = camera.matrix_world.col[2].xyz - - # Broadphase check using the bounding box - bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box] - bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords] - - pos_exists_bb = any(d > 0 for d in bounding_box_signed_distances) - neg_exists_bb = any(d < 0 for d in bounding_box_signed_distances) - - if not (pos_exists_bb and neg_exists_bb): - return False - - bm = bmesh.new() - bm.from_mesh(obj.data) - - # Transform the vertices to world space - mesh_mat = obj.matrix_world - bm.transform(mesh_mat) - - # Calculate the signed distances of all vertices from the plane - signed_distances = [plane_no.dot(v.co - plane_co) for v in bm.verts] - - bm.free() - - # Check for intersection - pos_exists = any(d > 0 for d in signed_distances) - neg_exists = any(d < 0 for d in signed_distances) - - return pos_exists and neg_exists - - def bisect_mesh(self, obj, camera): - camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world - plane_co = camera_matrix.translation - plane_no = camera_matrix.col[2].xyz - - global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start - - bm = bmesh.new() - bm.from_mesh(obj.data) - - # Run the bisect operation - geom = bm.verts[:] + bm.edges[:] + bm.faces[:] - results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no) - - vert_map = {} - verts = [] - edges = [] - i = 0 - for geom in results["geom_cut"]: - if isinstance(geom, bmesh.types.BMVert): - verts.append(tuple((obj.matrix_world @ geom.co) + global_offset)) - vert_map[geom.index] = i - i += 1 - else: - # It seems as though edges always appear after verts - edges.append([vert_map[v.index] for v in geom.verts]) - - bm.free() - - return verts, edges - class DecorationsHandler: decorators_classes = [ diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 6306f3cd90..b431a8fcfb 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -485,10 +485,13 @@ class CreateDrawing(bpy.types.Operator): self.serialiser.finalize() results = self.svg_buffer.get_value() + root = etree.fromstring(results) + self.move_projection_to_bottom(root) + self.merge_linework_and_add_metadata(root) + if self.camera.data.BIMCameraProperties.calculate_shapely_surfaces: # shapely variant - root = etree.fromstring(results) - group = root.findall('.//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}name]')[0] + group = root.findall(".//{http://www.w3.org/2000/svg}g")[0] nm = group.attrib["{http://www.ifcopenshell.org/ns}name"] m4 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}plane"])) m3 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}matrix3"])) @@ -503,7 +506,7 @@ class CreateDrawing(bpy.types.Operator): for projection in projections: boundary_lines = [] - for path in projection.findall('./{http://www.w3.org/2000/svg}path'): + for path in projection.findall("./{http://www.w3.org/2000/svg}path"): start, end = [co[1:].split(",") for co in path.attrib["d"].split()] boundary_lines.append(shapely.LineString([start, end])) unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) @@ -517,23 +520,39 @@ class CreateDrawing(bpy.types.Operator): internal_point = centroid if polygon.contains(centroid) else polygon.representative_point() if internal_point: internal_point = [internal_point.x, internal_point.y] - a, b = self.drawing_to_model_co(m44, m4, internal_point, 0.0), self.drawing_to_model_co(m44, m4, internal_point, -100.0) + a, b = self.drawing_to_model_co(m44, m4, internal_point, 0.0), self.drawing_to_model_co( + m44, m4, internal_point, -100.0 + ) inside_elements = [e for e in tree.select(self.pythonize(a)) if not e.is_a("IfcAnnotation")] if not inside_elements: - elements = [e for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) if not e.instance.is_a("IfcAnnotation")] + elements = [ + e + for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) + if not e.instance.is_a("IfcAnnotation") + ] if elements: - path = etree.SubElement(group, "path") - d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z" + path = etree.Element("path") + d = ( + "M" + + " L".join( + [",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]] + ) + + " Z" + ) for interior in polygon.interiors: - d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z" + d += ( + " M" + + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + + " Z" + ) path.attrib["d"] = d classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id())) classes.append("surface") path.set("class", " ".join(list(classes))) - - results = etree.tostring(root) + group.insert(0, path) if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces: + results = etree.tostring(root).decode("utf8") svg_data_1 = results from xml.dom.minidom import parseString @@ -624,7 +643,11 @@ class CreateDrawing(bpy.types.Operator): if iteration != num_passes: semantics[pi] = (inside_elements[0], -1) else: - elements = [e for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) if not e.instance.is_a("IfcAnnotation")] + elements = [ + e + for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) + if not e.instance.is_a("IfcAnnotation") + ] if elements: classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id())) @@ -682,16 +705,56 @@ class CreateDrawing(bpy.types.Operator): # This generally shouldn't happen g1.appendChild(g2) - data = dom1.toxml() - data = data.encode("ascii", "xmlcharrefreplace") - - results = data - with profile("Post processing"): + results = dom1.toxml() + results = results.encode("ascii", "xmlcharrefreplace") root = etree.fromstring(results) - self.move_projection_to_bottom(root) - self.merge_linework_and_add_metadata(root) - with open(svg_path, "wb") as svg: - svg.write(etree.tostring(root)) + + # Spaces are handled as a special case, since they are often overlayed + # in addition to elements. For example, a space should not obscure + # other elements in projection. Spaces should also not override cut + # elements but instead be drawn in addition to cut elements. + spaces = tool.Drawing.get_drawing_spaces(self.camera_element) + + group = root.findall(".//{http://www.w3.org/2000/svg}g")[0] + + self.svg_writer.calculate_scale() + x_offset = self.svg_writer.raw_width / 2 + y_offset = self.svg_writer.raw_height / 2 + + for space in spaces: + obj = tool.Ifc.get_object(space) + if not obj or not tool.Drawing.is_intersecting_camera(obj, self.camera): + continue + verts, edges = tool.Drawing.bisect_mesh(obj, self.camera) + verts = [self.svg_writer.project_point_onto_camera(Vector(v)) for v in verts] + line_strings = [ + shapely.LineString( + [ + ( + (x_offset + verts[e[0]][0]) * self.svg_writer.svg_scale, + (y_offset - verts[e[0]][1]) * self.svg_writer.svg_scale, + ), + ( + (x_offset + verts[e[1]][0]) * self.svg_writer.svg_scale, + (y_offset - verts[e[1]][1]) * self.svg_writer.svg_scale, + ), + ] + ) + for e in edges + ] + closed_polygons = shapely.polygonize(line_strings) + for polygon in closed_polygons.geoms: + classes = self.get_svg_classes(space) + path = etree.Element("path") + d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z" + for interior in polygon.interiors: + d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z" + path.attrib["d"] = d + path.set("class", " ".join(list(classes))) + group.append(path) + + with open(svg_path, "wb") as svg: + svg.write(etree.tostring(root)) return svg_path @@ -748,13 +811,12 @@ class CreateDrawing(bpy.types.Operator): value = ifcopenshell.util.selector.get_element_value(element, key) if value: classes.append( - tool.Drawing.canonicalise_class_name(key) - + "-" - + tool.Drawing.canonicalise_class_name(str(value)) + tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value)) ) return classes def merge_linework_and_add_metadata(self, root): + group = root.findall(".//{http://www.w3.org/2000/svg}g")[0] joined_paths = {} ifc = tool.Ifc.get() @@ -796,16 +858,15 @@ class CreateDrawing(bpy.types.Operator): elif type(merged_polygons) == shapely.Polygon: merged_polygons = [merged_polygons] - root_g = root.findall(".//{http://www.w3.org/2000/svg}g")[0] for polygon in merged_polygons: - g = etree.SubElement(root, "g") + g = etree.Element("g") path = etree.SubElement(g, "path") d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z" for interior in polygon.interiors: d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z" path.attrib["d"] = d g.set("class", " ".join(list(classes))) - root_g.append(g) + group.append(g) def drawing_to_model_co(self, m44, m4, xy, z=0.0): xyzw = m44 @ np.array(xy + [z, 1.0]) @@ -816,14 +877,12 @@ class CreateDrawing(bpy.types.Operator): return tuple(map(float, arr)) def move_projection_to_bottom(self, root): - # https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value + # IfcConvert puts the projection afterwards which is not correct since + # projection should be drawn underneath the cut. group = root.find("{http://www.w3.org/2000/svg}g") - if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces: - # SVGFill already places the projection in the right spot. - return - # group[:] = sorted(group, key=lambda e : "projection" in e.get("class")) - if group is not None: - group[:] = reversed(group) + projection = group.find("{http://www.w3.org/2000/svg}g[@class='projection']") + projection.getparent().remove(projection) + group.insert(0, projection) def generate_annotation(self, context): if not self.cprops.has_annotation: diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index b24fb2d4ce..15eb3902ab 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1342,6 +1342,7 @@ class Drawing(blenderbim.core.tool.Drawing): elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement")) else: elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement")) + elements = {e for e in elements if e.is_a() != "IfcSpace"} annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) elements.update(annotations) @@ -1351,6 +1352,19 @@ class Drawing(blenderbim.core.tool.Drawing): elements -= set(ifc_file.by_type("IfcOpeningElement")) return elements + @classmethod + def get_drawing_spaces(cls, drawing): + ifc_file = tool.Ifc.get() + pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}) + include = pset.get("Include", None) + elements = set(ifc_file.by_type("IfcSpace")) + if include: + elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements)) + exclude = pset.get("Exclude", None) + if exclude: + elements -= set(ifcopenshell.util.selector.Selector.parse(ifc_file, exclude, elements=elements)) + return elements + @classmethod def get_annotation_element(cls, element): for rel in element.HasAssignments: @@ -1464,3 +1478,69 @@ class Drawing(blenderbim.core.tool.Drawing): should_sync_changes_first=True, ) break + + @classmethod + def is_intersecting_camera(cls, obj, camera): + # Based on separating axis theorem + plane_co = camera.matrix_world.translation + plane_no = camera.matrix_world.col[2].xyz + + # Broadphase check using the bounding box + bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box] + bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords] + + pos_exists_bb = any(d > 0 for d in bounding_box_signed_distances) + neg_exists_bb = any(d < 0 for d in bounding_box_signed_distances) + + if not (pos_exists_bb and neg_exists_bb): + return False + + bm = bmesh.new() + bm.from_mesh(obj.data) + + # Transform the vertices to world space + mesh_mat = obj.matrix_world + bm.transform(mesh_mat) + + # Calculate the signed distances of all vertices from the plane + signed_distances = [plane_no.dot(v.co - plane_co) for v in bm.verts] + + bm.free() + + # Check for intersection + pos_exists = any(d > 0 for d in signed_distances) + neg_exists = any(d < 0 for d in signed_distances) + + return pos_exists and neg_exists + + @classmethod + def bisect_mesh(cls, obj, camera): + camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world + plane_co = camera_matrix.translation + plane_no = camera_matrix.col[2].xyz + + global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start + + bm = bmesh.new() + bm.from_mesh(obj.data) + + # Run the bisect operation + geom = bm.verts[:] + bm.edges[:] + bm.faces[:] + results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no) + + vert_map = {} + verts = [] + edges = [] + i = 0 + for geom in results["geom_cut"]: + if isinstance(geom, bmesh.types.BMVert): + verts.append(tuple((obj.matrix_world @ geom.co) + global_offset)) + vert_map[geom.index] = i + i += 1 + else: + # It seems as though edges always appear after verts + edges.append([vert_map[v.index] for v in geom.verts]) + + bm.free() + + return verts, edges