diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 4806b2e195..3ed26b7389 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -1,45 +1,6 @@ VERSION:=`date '+%y%m%d'` PYVERSION:=py37 -ifeq ($(PLATFORM), win) -ifeq ($(PYVERSION), py37) -PYTHONOCC_URL:=https://anaconda.org/DLR-SC/pythonocc-core/0.17.3/download/win-64/pythonocc-core-0.17.3-py37he980bc4_10.tar.bz2 -OCE_URL:=https://anaconda.org/DLR-SC/oce/0.17.2/download/win-64/oce-0.17.2-he980bc4_14.tar.bz2 -TBB_URL:=https://anaconda.org/DLR-SC/tbb/2019.5/download/win-64/tbb-2019.5-he980bc4_0.tar.bz2 -endif -ifeq ($(PYVERSION), py39) -PYTHONOCC_URL:=https://anaconda.org/conda-forge/pythonocc-core/7.4.1/download/win-64/pythonocc-core-7.4.1-py39h3d1c7c5_0.tar.bz2 -OCE_URL:=https://anaconda.org/conda-forge/occt/7.4.0/download/win-64/occt-7.4.0-h823b557_3.tar.bz2 -TBB_URL:=https://anaconda.org/conda-forge/tbb/2020.2/download/win-64/tbb-2020.2-h2d74725_4.tar.bz2 -endif -endif - -ifeq ($(PLATFORM), macos) -ifeq ($(PYVERSION), py37) -PYTHONOCC_URL:=https://anaconda.org/DLR-SC/pythonocc-core/0.17.3/download/osx-64/pythonocc-core-0.17.3-py37h04f5b5a_10.tar.bz2 -OCE_URL:=https://anaconda.org/DLR-SC/oce/0.17.2/download/osx-64/oce-0.17.2-h04f5b5a_12.tar.bz2 -TBB_URL:=https://anaconda.org/DLR-SC/tbb/4.3.6/download/osx-64/tbb-4.3.6-0.tar.bz2 -endif -ifeq ($(PYVERSION), py39) -PYTHONOCC_URL:=https://anaconda.org/conda-forge/pythonocc-core/7.4.1/download/osx-64/pythonocc-core-7.4.1-py39h4d29fe3_0.tar.bz2 -OCE_URL:=https://anaconda.org/conda-forge/occt/7.4.0/download/osx-64/occt-7.4.0-hb9b6dc7_3.tar.bz2 -TBB_URL:=https://anaconda.org/conda-forge/tbb/2020.2/download/osx-64/tbb-2020.2-h940c156_4.tar.bz2 -endif -endif - -ifeq ($(PLATFORM), linux) -ifeq ($(PYVERSION), py37) -PYTHONOCC_URL:=https://anaconda.org/DLR-SC/pythonocc-core/0.17.3/download/linux-64/pythonocc-core-0.17.3-py37h6bb024c_10.tar.bz2 -OCE_URL:=https://anaconda.org/DLR-SC/oce/0.17.2/download/linux-64/oce-0.17.2-h6bb024c_14.tar.bz2 -TBB_URL:=https://anaconda.org/DLR-SC/tbb/4.3.6/download/linux-64/tbb-4.3.6-0.tar.bz2 -endif -ifeq ($(PYVERSION), py39) -PYTHONOCC_URL:=https://anaconda.org/conda-forge/pythonocc-core/7.4.1/download/linux-64/pythonocc-core-7.4.1-py39h465cb30_0.tar.bz2 -OCE_URL:=https://anaconda.org/conda-forge/occt/7.4.0/download/linux-64/occt-7.4.0-h9121d39_3.tar.bz2 -TBB_URL:=https://anaconda.org/conda-forge/tbb/2020.2/download/linux-64/tbb-2020.2-h4bd325d_4.tar.bz2 -endif -endif - .PHONY: dist dist: ifndef PLATFORM @@ -76,66 +37,6 @@ else endif rm -rf dist/working - # Provides Python OCC functionality for cutting IFC geometry for construction documentation - mkdir dist/working - cd dist/working && wget $(PYTHONOCC_URL) - cd dist/working && tar -xjvf pythonocc-core* -ifeq ($(PLATFORM), win) - cd dist/working && cp -r Lib/site-packages/OCC ../blenderbim/libs/site/packages/ -else -ifeq ($(PYVERSION), py37) - cd dist/working && cp -r lib/python3.7/site-packages/OCC ../blenderbim/libs/site/packages/ -endif -ifeq ($(PYVERSION), py39) - cd dist/working && cp -r lib/python3.9/site-packages/OCC ../blenderbim/libs/site/packages/ -endif -endif - rm -rf dist/working - - # Required by Python OCC - mkdir dist/working - cd dist/working && wget $(OCE_URL) - cd dist/working && tar -xjvf oc* -ifeq ($(PLATFORM), win) -ifeq ($(PYVERSION), py37) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/ -endif -ifeq ($(PYVERSION), py39) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/Core/ -endif -else - # Unix Conda builds of PythonOCC expect OCE libs to have a RPATH of ../../../ - cd dist/working && cp -r lib/* ../blenderbim/libs/ -endif -ifeq ($(PLATFORM), linux) - rm -rf dist/blenderbim/libs/oce-0.17 -endif - rm -rf dist/working - - # Required by OpenCascade - mkdir dist/working - cd dist/working && wget $(TBB_URL) - cd dist/working && tar -xjvf tbb* -ifeq ($(PLATFORM), win) -ifeq ($(PYVERSION), py37) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/ -endif -ifeq ($(PYVERSION), py39) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/Core/ -endif -else - cd dist/working && cp -r lib/* ../blenderbim/libs/ -endif - rm -rf dist/working - -ifeq ($(PLATFORM), macos) - mkdir dist/working - cd dist/working && wget https://blenderbim.org/builds/patch-blender28-bim-macos.zip - cd dist/working && unzip patch* - cd dist/working && cp -r *.dylib ../blenderbim/libs/ - rm -rf dist/working -endif - # Provides dependencies that are part of IfcOpenShell mkdir dist/working cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.6.0.zip diff --git a/src/blenderbim/blenderbim/__init__.py b/src/blenderbim/blenderbim/__init__.py index e26493b051..3251fdd873 100644 --- a/src/blenderbim/blenderbim/__init__.py +++ b/src/blenderbim/blenderbim/__init__.py @@ -15,6 +15,7 @@ import site # process *.pth in /libs/site/packages to setup globally importable modules # 3 levels deep required by occ static ../../ path +# TODO: 3 levels deep is no longer required as we no longer bundle OCC cwd = os.path.dirname(os.path.realpath(__file__)) site.addsitedir(os.path.join(cwd, "libs", "site", "packages")) diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 6bb3ead78b..57a386b15c 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -73,107 +73,3 @@ def export_attributes(props, callback=None): elif attribute.data_type == "enum": attributes[attribute.name] = attribute.enum_value return attributes - - -# TODO: migrate the below helper functions into the drawing module, since it is specific to that module - - -def parse_diagram_scale(camera): - """Returns numeric value of scale""" - if camera.BIMCameraProperties.diagram_scale == "CUSTOM": - _, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|") - else: - _, fraction = camera.BIMCameraProperties.diagram_scale.split("|") - numerator, denominator = fraction.split("/") - return float(numerator) / float(denominator) - - -def ortho_view_frame(camera, margin=0.015): - """Calculates 2d bounding box of camera view area. - - Similar to `bpy.types.Camera.view_frame` - - :arg camera: camera of drawing - :type camera: bpy.types.Camera + BIMCameraProperties - :arg margin: margins, in scene units - :type margin: float - :return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates - """ - aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x - size = camera.ortho_scale - hwidth = size * 0.5 - hheight = size * 0.5 * aspect - scale = parse_diagram_scale(camera) - xmarg = margin * scale - ymarg = margin * scale * aspect - return (-hwidth + xmarg, hwidth - xmarg, -hheight + ymarg, hheight - ymarg, -camera.clip_start, -camera.clip_end) - - -def almost_zero(v): - return abs(v) < 1e-5 - - -def clip_segment(bounds, segm): - """Clipping line segment to bounds - - :arg bounds: (xmin, xmax, ymin, ymax) - :arg segm: 2 vertices of the segment - :return: 2 new vertices of segment or None if segment outside the bounding box - """ - # Liang–Barsky algorithm - - xmin, xmax, ymin, ymax, _, _ = bounds - p1, p2 = segm - - def clip_side(p, q): - if almost_zero(p): # ~= 0, parallel to the side - if q < 0: - return None # outside - else: - return 0, 1 # inside - - t = q / p # the intersection point - - if p < 0: # entering - return t, 1 - else: # leaving - return 0, t - - dlt = p2 - p1 - - tt = ( - clip_side(-dlt.x, p1.x - xmin), # left - clip_side(+dlt.x, xmax - p1.x), # right - clip_side(-dlt.y, p1.y - ymin), # bottom - clip_side(+dlt.y, ymax - p1.y), # top - ) - - if None in tt: - return None - - t1 = max(0, max(t[0] for t in tt)) - t2 = min(1, min(t[1] for t in tt)) - - if t1 >= t2: - return None - - p1c = p1 + dlt * t1 - p2c = p1 + dlt * t2 - - return p1c, p2c - - -def elevate_segment(bounds, segm): - """Elevate line xy-perpendicular segment vertically - - :arg bounds: (xmin, xmax, ymin, ymax) - :arg segm: 2 vertices of the segment - :return: 2 new vertices of segment or None if segment outside the bounding box - """ - _, _, ymin, ymax, zmin, _ = bounds - p1, p2 = segm - dlt = p2 - p1 - if not (almost_zero(dlt.x) and almost_zero(dlt.y)): - return None - x = p1.x - return [Vector((x, ymin, zmin)), Vector((x, ymax, zmin))] diff --git a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py index a871fad0c5..b7abbc2ede 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py @@ -5,7 +5,6 @@ classes = ( operator.AddDrawing, operator.CreateDrawing, operator.AddAnnotation, - operator.CutSection, # TODO: To deprecate operator.AddSheet, operator.OpenSheet, operator.AddDrawingToSheet, diff --git a/src/blenderbim/blenderbim/bim/module/drawing/cut_ifc.py b/src/blenderbim/blenderbim/bim/module/drawing/cut_ifc.py deleted file mode 100644 index 2fbe91a29a..0000000000 --- a/src/blenderbim/blenderbim/bim/module/drawing/cut_ifc.py +++ /dev/null @@ -1,552 +0,0 @@ -import os -import re -import math -import time -import numpy -import pickle -import multiprocessing - - -def load_occ(): - # Don't import until we really need to, as a temporary step before we can purge OCC - try: - from OCC.Core import ( - gp, - Geom, - Bnd, - BRepBndLib, - BRep, - BRepPrimAPI, - BRepAlgoAPI, - BRepBuilderAPI, - TopOpeBRepTool, - TopOpeBRepBuild, - ShapeExtend, - GProp, - BRepGProp, - GC, - ShapeAnalysis, - TopTools, - TopExp, - TopAbs, - HLRAlgo, - HLRBRep, - TopLoc, - Bnd, - BRepBndLib, - BRepTools, - TopoDS, - GeomLProp, - IntCurvesFace, - ) - from OCC.Core.TopoDS import topods - except ImportError: - from OCC import ( - gp, - Geom, - Bnd, - BRepBndLib, - BRep, - BRepPrimAPI, - BRepAlgoAPI, - BRepBuilderAPI, - TopOpeBRepTool, - TopOpeBRepBuild, - ShapeExtend, - GProp, - BRepGProp, - GC, - ShapeAnalysis, - TopTools, - TopExp, - TopAbs, - HLRAlgo, - HLRBRep, - TopLoc, - Bnd, - BRepBndLib, - BRepTools, - TopoDS, - GeomLProp, - IntCurvesFace, - ) - from OCC.TopoDS import topods - -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.util.selector -import ifcopenshell.util.element - -cwd = os.path.dirname(os.path.realpath(__file__)) -this_file = os.path.join(cwd, "cut_ifc.py") - - -def get_booleaned_edges(shape): - load_occ() - edges = [] - exp = TopExp.TopExp_Explorer(shape, TopAbs.TopAbs_EDGE) - while exp.More(): - edges.append(topods.Edge(exp.Current())) - exp.Next() - return edges - - -def connect_edges_into_wires(unconnected_edges): - load_occ() - edges = TopTools.TopTools_HSequenceOfShape() - edges_handle = TopTools.Handle_TopTools_HSequenceOfShape(edges) - wires = TopTools.TopTools_HSequenceOfShape() - wires_handle = TopTools.Handle_TopTools_HSequenceOfShape(wires) - - for edge in unconnected_edges: - edges.Append(edge) - - ShapeAnalysis.ShapeAnalysis_FreeBounds.ConnectEdgesToWires(edges_handle, 1e-5, True, wires_handle) - return wires_handle.GetObject() - - -def do_cut(process_data): - load_occ() - global_id, shape, section, trsf_data = process_data - - axis = gp.gp_Ax2( - gp.gp_Pnt(trsf_data["top_left_corner"][0], trsf_data["top_left_corner"][1], trsf_data["top_left_corner"][2]), - gp.gp_Dir(trsf_data["projection"][0], trsf_data["projection"][1], trsf_data["projection"][2]), - gp.gp_Dir(trsf_data["x_axis"][0], trsf_data["x_axis"][1], trsf_data["x_axis"][2]), - ) - source = gp.gp_Ax3(axis) - destination = gp.gp_Ax3(gp.gp_Pnt(0, 0, 0), gp.gp_Dir(0, 0, -1), gp.gp_Dir(1, 0, 0)) - transformation = gp.gp_Trsf() - transformation.SetDisplacement(source, destination) - - cut_polygons = [] - section = BRepAlgoAPI.BRepAlgoAPI_Section(section, shape).Shape() - section_edges = get_booleaned_edges(section) - if len(section_edges) <= 0: - return cut_polygons - wires = connect_edges_into_wires(section_edges) - for i in range(wires.Length()): - wire_shape = wires.Value(i + 1) - - transformed_wire = BRepBuilderAPI.BRepBuilderAPI_Transform(wire_shape, transformation) - wire_shape = transformed_wire.Shape() - - wire = topods.Wire(wire_shape) - face = BRepBuilderAPI.BRepBuilderAPI_MakeFace(wire).Face() - - points = [] - exp = BRepTools.BRepTools_WireExplorer(wire) - while exp.More(): - point = BRep.BRep_Tool.Pnt(exp.CurrentVertex()) - points.append((point.X(), -point.Y())) - exp.Next() - cut_polygons.append({"global_id": global_id, "metadata": {}, "points": points}) - return cut_polygons - - -class IfcCutter: - def __init__(self): - self.time = None - self.selector = ifcopenshell.util.selector.Selector() - self.product_shapes = [] - self.background_elements = [] - self.cut_polygons = [] - self.template_variables = {} - self.metadata = {} - self.data_dir = "" - self.vector_style = "" - self.ifc_filenames = [] - self.ifc_files = {} - self.resolved_pixels = set() - self.text_pickle_file = "text.pickle" - self.metadata_pickle_file = "metadata.pickle" - self.cut_pickle_file = "cut.pickle" - self.should_recut = True - self.should_recut_selected = True - self.cut_objects = "" - self.selected_global_ids = [] - self.should_extract = True - self.diagram_name = None - self.background_image = None - self.section_box = { - "projection": (0, 1, 0), - "x_axis": (1, 0, 0), - "y_axis": (0, 0, -1), - "top_left_corner": (-2, 2, 8), - "x": 14, - "y": 9, - "z": 2, - "shape": None, - "face": None, - } - - def cut(self): - self.profile_code("Starting cut process") - self.load_ifc_files() - self.profile_code("Load IFC files") - self.get_template_variables() - self.profile_code("Get template variables") - self.get_product_shapes() - self.profile_code("Get product shapes") - self.create_section_box() - self.profile_code("Create section box") - self.get_cut_polygons() - self.profile_code("Get cut polygons") - self.get_annotation() - self.profile_code("Get annotation") - self.get_cut_polygon_metadata() - self.profile_code("Get cut polygon metadata") - - def profile_code(self, message): - if not self.time: - self.time = time.time() - print("{} :: {:.2f}".format(message, time.time() - self.time)) - self.time = time.time() - - def load_ifc_files(self): - if not self.should_recut and not self.should_extract: - return - - loaded_files = [] - for filename in self.ifc_filenames: - print("Loading file {} ...".format(filename)) - if filename: - self.ifc_files[filename] = ifcopenshell.open(filename) - - def get_template_variables(self): - if not self.should_extract: - if os.path.isfile(self.text_pickle_file): - with open(self.text_pickle_file, "rb") as text_file: - self.template_variables = pickle.load(text_file) - return - - data = {} - for text_obj in self.text_objs: - text_obj_data = self.get_text_variables(text_obj) - if text_obj_data: - data[text_obj.name] = text_obj_data - - with open(self.text_pickle_file, "wb") as text_file: - pickle.dump(data, text_file, protocol=pickle.HIGHEST_PROTOCOL) - - self.template_variables = data - - def get_text_variables(self, text_obj): - text_obj_data = {} - text_body = text_obj.data.body - related_element = text_obj.data.BIMTextProperties.related_element - if not related_element: - return - global_id = related_element.BIMObjectProperties.attributes.get("GlobalId") - if not global_id: - return - element = self.get_ifc_element(global_id.string_value) - for variable in text_obj.data.BIMTextProperties.variables: - if element: - if "{{" in variable.prop_key: - prop_key = variable.prop_key.split("{{")[1].split("}}")[0] - prop_value = self.selector.get_element_value(element, prop_key) - variable_value = eval(variable.prop_key.replace("{{" + prop_key + "}}", str(prop_value))) - else: - variable_value = self.selector.get_element_value(element, variable.prop_key) - text_obj_data[variable.name] = variable_value - return text_obj_data - - def get_product_shapes(self): - if not self.should_recut: - return - - settings = ifcopenshell.geom.settings() - settings.set(settings.USE_PYTHON_OPENCASCADE, True) - products = [] - - for filename, ifc_file in self.ifc_files.items(): - shape_pickle = os.path.join( - self.data_dir, "cache", "shapes", "{}.pickle".format(os.path.basename(filename)) - ) - shape_map = {} - if self.should_recut_selected and os.path.isfile(shape_pickle): - with open(shape_pickle, "rb") as shape_file: - shape_map = pickle.load(shape_file) - - products.extend(self.selector.parse(ifc_file, self.cut_objects)) - - selected_elements = [] - for i, product in enumerate(products): - if ( - product.is_a("IfcOpeningElement") - or product.is_a("IfcSite") - or product.Representation is None - or self.has_annotation(product) - ): - continue - try: - if self.should_recut_selected and product.GlobalId in self.selected_global_ids: - selected_elements.append(product) - elif product.GlobalId in shape_map: - shape = shape_map[product.GlobalId] - self.add_product_shape(product, shape) - else: - selected_elements.append(product) - except: - print("Failed to create shape for {}".format(product)) - - if selected_elements: - total = 0 - checkpoint = time.time() - iterator = ifcopenshell.geom.iterator( - settings, ifc_file, multiprocessing.cpu_count(), include=selected_elements - ) - valid_file = iterator.initialize() - if valid_file: - while True: - total += 1 - if total % 250 == 0: - print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint)) - checkpoint = time.time() - shape = iterator.get() - shape_map[shape.data.guid] = shape.geometry - self.add_product_shape(ifc_file.by_guid(shape.data.guid), shape.geometry) - if not iterator.next(): - break - - with open(shape_pickle, "wb") as shape_file: - pickle.dump(shape_map, shape_file, protocol=pickle.HIGHEST_PROTOCOL) - - def add_product_shape(self, product, shape): - self.product_shapes.append((product, shape)) - - def has_annotation(self, element): - for representation in element.Representation.Representations: - if ( - representation.ContextOfItems.ContextType == "Plan" - and representation.ContextOfItems.ContextIdentifier == "Annotation" - ): - return True - return False - - def create_section_box(self): - load_occ() - top_left_corner = gp.gp_Pnt( - self.section_box["top_left_corner"][0], - self.section_box["top_left_corner"][1], - self.section_box["top_left_corner"][2], - ) - axis = gp.gp_Ax2( - top_left_corner, - gp.gp_Dir( - self.section_box["projection"][0], self.section_box["projection"][1], self.section_box["projection"][2] - ), - gp.gp_Dir(self.section_box["x_axis"][0], self.section_box["x_axis"][1], self.section_box["x_axis"][2]), - ) - section_box = BRepPrimAPI.BRepPrimAPI_MakeBox( - axis, self.section_box["x"], self.section_box["y"], self.section_box["z"] - ) - self.section_box["shape"] = section_box.Shape() - self.section_box["face"] = section_box.BottomFace() - - source = gp.gp_Ax3(axis) - self.transformation_data = { - "top_left_corner": self.section_box["top_left_corner"], - "projection": self.section_box["projection"], - "x_axis": self.section_box["x_axis"], - } - destination = gp.gp_Ax3(gp.gp_Pnt(0, 0, 0), gp.gp_Dir(0, 0, -1), gp.gp_Dir(1, 0, 0)) - self.transformation_dest = destination - self.transformation = gp.gp_Trsf() - self.transformation.SetDisplacement(source, destination) - - def get_bbox(self, shape): - load_occ() - bbox = Bnd.Bnd_Box() - BRepBndLib.brepbndlib_Add(shape, bbox) - return bbox - - def calculate_face_zpos(self, face): - bbox = self.get_bbox(face) - xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() - zpos = zmin + ((zmax - zmin) / 2) - return zpos, zmax - - def get_booleaned_edges(self, shape): - load_occ() - edges = [] - exp = TopExp.TopExp_Explorer(shape, TopAbs.TopAbs_EDGE) - while exp.More(): - edges.append(topods.Edge(exp.Current())) - exp.Next() - return edges - - def get_cut_polygons(self): - if self.should_recut: - self.get_fresh_cut_polygons() - self.pickle_cut_polygons() - else: - self.get_pickled_cut_polygons() - - def get_annotation(self): - import mathutils - load_occ() - - self.annotation_objs = [] - settings_2d = ifcopenshell.geom.settings() - settings_2d.set(settings_2d.INCLUDE_CURVES, True) - settings_py = ifcopenshell.geom.settings() - settings_py.set(settings_py.USE_PYTHON_OPENCASCADE, True) - for ifc_file in self.ifc_files.values(): - for element in ifc_file.by_type("IfcElement"): - annotation_representation = None - box_representation = None - if not element.Representation: - continue # This can occur for aggregates - for representation in element.Representation.Representations: - if ( - representation.ContextOfItems.ContextType == "Plan" - and representation.ContextOfItems.ContextIdentifier == "Annotation" - ): - annotation_representation = representation - elif ( - representation.ContextOfItems.ContextType == "Model" - and representation.ContextOfItems.ContextIdentifier == "Box" - ): - box_representation = representation - if not annotation_representation or not box_representation: - continue - - # This is bad code. See bug #85 to make it slightly less bad. - # Effectively if the bbox does not intersect with the camera - # plane, then we should "continue" and not process the 2D - # wireframe. This approach works but is not very smart. - for subelement in ifc_file.traverse(box_representation): - if subelement.is_a("IfcBoundingBox"): - block = ifc_file.createIfcBlock( - ifc_file.createIfcAxis2Placement3D(subelement.Corner, None, None), - subelement.XDim, - subelement.YDim, - subelement.ZDim, - ) - for inverse in ifc_file.get_inverse(subelement): - ifcopenshell.util.element.replace_attribute(inverse, subelement, block) - element.Representation.Representations = [box_representation] - shape = ifcopenshell.geom.create_shape(settings_py, element) - - section = BRepAlgoAPI.BRepAlgoAPI_Section(self.section_box["face"], shape.geometry).Shape() - section_edges = get_booleaned_edges(section) - - if len(section_edges) <= 0: - # The bounding box of the annotation object does not - # intersect with the camera plane, so don't bother drawing - # the annotation - continue - - # Monkey patch - see bug #771. - element.Representation.Representations = [annotation_representation] - shape = ifcopenshell.geom.create_shape(settings_2d, element) - if hasattr(shape, "geometry"): - geometry = shape.geometry - else: - geometry = shape - e = geometry.edges - v = geometry.verts - m = shape.transformation.matrix.data - mat = mathutils.Matrix( - ([m[0], m[1], m[2], 0], [m[3], m[4], m[5], 0], [m[6], m[7], m[8], 0], [m[9], m[10], m[11], 1]) - ) - mat.transpose() - self.annotation_objs.append( - { - "raw": element, - "classes": self.get_classes(element, "annotation"), - "edges": [[e[i], e[i + 1]] for i in range(0, len(e), 2)], - "vertices": [mat @ mathutils.Vector((v[i], v[i + 1], v[i + 2])) for i in range(0, len(v), 3)], - } - ) - - def get_cut_polygon_metadata(self): - if not self.should_extract: - if os.path.isfile(self.metadata_pickle_file): - with open(self.metadata_pickle_file, "rb") as metadata_file: - self.metadata = pickle.load(metadata_file) - - for polygon in self.cut_polygons: - if polygon["global_id"] in self.metadata: - polygon["metadata"] = self.metadata[polygon["global_id"]] - return - - for polygon in self.cut_polygons: - metadata = {"classes": self.get_classes(self.get_ifc_element(polygon["global_id"]), "cut")} - self.metadata[polygon["global_id"]] = metadata - polygon["metadata"] = metadata - - with open(self.metadata_pickle_file, "wb") as metadata_file: - pickle.dump(self.metadata, metadata_file, protocol=pickle.HIGHEST_PROTOCOL) - - def pickle_cut_polygons(self): - with open(self.cut_pickle_file, "wb") as pickle_file: - pickle.dump(self.cut_polygons, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) - - def get_fresh_cut_polygons(self): - process_data = [ - (p.GlobalId, s, self.section_box["face"], self.transformation_data) for p, s in self.product_shapes - ] - - import bpy - - if bpy.app.version > (2, 90, 0) and os.name == 'nt': - # See bug #1148 - for data in process_data: - results = do_cut(data) - polygons = [r for r in results if r["points"]] - self.cut_polygons.extend(polygons) - else: - multiprocessing.set_executable(bpy.app.binary_path_python) - - with multiprocessing.Pool(9) as p: - results = p.map(do_cut, process_data) - for result in results: - polygons = [p for p in result if p["points"]] - self.cut_polygons.extend(polygons) - - def get_polygon_metadata(self, polygon, position): - polygon["metadata"] = {"classes": self.get_classes(self.get_ifc_element(polygon["global_id"]), position)} - return polygon - - def get_ifc_element(self, global_id): - # TODO: make this less bad - element = None - for ifc_file in self.ifc_files.values(): - try: - element = ifc_file.by_id(global_id) - return element - except: - pass - - def get_classes(self, element, position): - classes = [position, element.is_a()] - material = ifcopenshell.util.element.get_material(element) - if material: - classes.append( - "material-{}".format( - re.sub("[^0-9a-zA-Z]+", "", self.get_material_name(material)) - ) - ) - classes.append("globalid-{}".format(element.GlobalId)) - for attribute in self.attributes: - result = self.selector.get_element_value(element, attribute) - if result: - classes.append( - "{}-{}".format(re.sub("[^0-9a-zA-Z]+", "", attribute), re.sub("[^0-9a-zA-Z]+", "", result)) - ) - return classes - - def get_material_name(self, element): - if hasattr(element, "Name") and element.Name: - return element.Name - elif hasattr(element, "LayerSetName") and element.LayerSetName: - return element.LayerSetName - return "mat-" + str(element.id()) - - def get_pickled_cut_polygons(self): - if os.path.isfile(self.cut_pickle_file): - with open(self.cut_pickle_file, "rb") as pickle_file: - self.cut_polygons = pickle.load(pickle_file) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index a71e549b23..6285251c4d 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -257,3 +257,104 @@ def get_project_collection(scene): if len(colls) != 1: raise RuntimeError("project collection missing or not unique") return colls[0] + + +def parse_diagram_scale(camera): + """Returns numeric value of scale""" + if camera.BIMCameraProperties.diagram_scale == "CUSTOM": + _, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|") + else: + _, fraction = camera.BIMCameraProperties.diagram_scale.split("|") + numerator, denominator = fraction.split("/") + return float(numerator) / float(denominator) + + +def ortho_view_frame(camera, margin=0.015): + """Calculates 2d bounding box of camera view area. + + Similar to `bpy.types.Camera.view_frame` + + :arg camera: camera of drawing + :type camera: bpy.types.Camera + BIMCameraProperties + :arg margin: margins, in scene units + :type margin: float + :return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates + """ + aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x + size = camera.ortho_scale + hwidth = size * 0.5 + hheight = size * 0.5 * aspect + scale = parse_diagram_scale(camera) + xmarg = margin * scale + ymarg = margin * scale * aspect + return (-hwidth + xmarg, hwidth - xmarg, -hheight + ymarg, hheight - ymarg, -camera.clip_start, -camera.clip_end) + + +def almost_zero(v): + return abs(v) < 1e-5 + + +def clip_segment(bounds, segm): + """Clipping line segment to bounds + + :arg bounds: (xmin, xmax, ymin, ymax) + :arg segm: 2 vertices of the segment + :return: 2 new vertices of segment or None if segment outside the bounding box + """ + # Liang–Barsky algorithm + + xmin, xmax, ymin, ymax, _, _ = bounds + p1, p2 = segm + + def clip_side(p, q): + if almost_zero(p): # ~= 0, parallel to the side + if q < 0: + return None # outside + else: + return 0, 1 # inside + + t = q / p # the intersection point + + if p < 0: # entering + return t, 1 + else: # leaving + return 0, t + + dlt = p2 - p1 + + tt = ( + clip_side(-dlt.x, p1.x - xmin), # left + clip_side(+dlt.x, xmax - p1.x), # right + clip_side(-dlt.y, p1.y - ymin), # bottom + clip_side(+dlt.y, ymax - p1.y), # top + ) + + if None in tt: + return None + + t1 = max(0, max(t[0] for t in tt)) + t2 = min(1, min(t[1] for t in tt)) + + if t1 >= t2: + return None + + p1c = p1 + dlt * t1 + p2c = p1 + dlt * t2 + + return p1c, p2c + + +def elevate_segment(bounds, segm): + """Elevate line xy-perpendicular segment vertically + + :arg bounds: (xmin, xmax, ymin, ymax) + :arg segm: 2 vertices of the segment + :return: 2 new vertices of segment or None if segment outside the bounding box + """ + _, _, ymin, ymax, zmin, _ = bounds + p1, p2 = segm + dlt = p2 - p1 + if not (almost_zero(dlt.x) and almost_zero(dlt.y)): + return None + x = p1.x + return [Vector((x, ymin, zmin)), Vector((x, ymax, zmin))] diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index e3d968bafd..3882d42dbe 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -10,7 +10,6 @@ import ifcopenshell.util.selector import ifcopenshell.util.representation import blenderbim.bim.module.drawing.svgwriter as svgwriter import blenderbim.bim.module.drawing.annotation as annotation -import blenderbim.bim.module.drawing.cut_ifc as cut_ifc # TODO: deprecate import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.scheduler as scheduler import blenderbim.bim.module.drawing.helper as helper @@ -353,202 +352,6 @@ class AddAnnotation(bpy.types.Operator): return {"FINISHED"} -class CutSection(bpy.types.Operator): - bl_idname = "bim.cut_section" - bl_label = "Cut Section" - - def execute(self, context): - self.file = ifc.IfcStore.get_file() - camera = bpy.context.scene.camera - if not (camera.type == "CAMERA" and camera.data.type == "ORTHO"): - return {"FINISHED"} - if not bpy.context.scene.DocProperties.ifc_files and bpy.context.scene.BIMProperties.ifc_file: - new = bpy.context.scene.DocProperties.ifc_files.add() - new.name = bpy.context.scene.BIMProperties.ifc_file - bpy.ops.bim.activate_view( - drawing_index=bpy.context.scene.DocProperties.drawings.find(camera.name.split("/")[1]) - ) - drawing_style = bpy.context.scene.DocProperties.drawing_styles[ - camera.data.BIMCameraProperties.active_drawing_style_index - ] - self.diagram_name = camera.name.split("/")[1] - bpy.context.scene.render.filepath = os.path.join( - bpy.context.scene.BIMProperties.data_dir, "diagrams", "{}.png".format(self.diagram_name) - ) - self.create_raster(camera, drawing_style) - location = camera.location - render = bpy.context.scene.render - if self.is_landscape(): - width = camera.data.ortho_scale - height = width / render.resolution_x * render.resolution_y - else: - height = camera.data.ortho_scale - width = height / render.resolution_y * render.resolution_x - depth = camera.data.clip_end - projection = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)) - x_axis = camera.matrix_world.to_quaternion() @ Vector((1, 0, 0)) - y_axis = camera.matrix_world.to_quaternion() @ Vector((0, -1, 0)) - top_left_corner = location - (width / 2 * x_axis) - (height / 2 * y_axis) - ifc_cutter = cut_ifc.IfcCutter() - - ifc_cutter.ifc_filenames = [i.name for i in bpy.context.scene.DocProperties.ifc_files] - ifc_cutter.data_dir = bpy.context.scene.BIMProperties.data_dir - ifc_cutter.vector_style = drawing_style.vector_style - ifc_cutter.diagram_name = self.diagram_name - ifc_cutter.background_image = bpy.context.scene.render.filepath - if camera.data.BIMCameraProperties.cut_objects == "CUSTOM": - ifc_cutter.cut_objects = camera.data.BIMCameraProperties.cut_objects_custom - else: - ifc_cutter.cut_objects = camera.data.BIMCameraProperties.cut_objects - ifc_cutter.leader_obj = None - ifc_cutter.stair_obj = None - ifc_cutter.dimension_objs = [] - ifc_cutter.break_obj = None - ifc_cutter.equal_objs = [] - ifc_cutter.hidden_objs = [] - ifc_cutter.solid_objs = [] - ifc_cutter.plan_level_obj = None - ifc_cutter.section_level_obj = None - ifc_cutter.grid_objs = [] - ifc_cutter.text_objs = [] - ifc_cutter.misc_objs = [] - ifc_cutter.attributes = [a.name for a in drawing_style.attributes] - for obj in camera.users_collection[0].objects: - if "IfcGrid" in obj.name: - ifc_cutter.grid_objs.append(obj) - elif "IfcGroup" in obj.name and obj.type == "CAMERA": - ifc_cutter.camera_obj = obj - - if "IfcAnnotation/" not in obj.name: - continue - - if "Leader" in obj.name: - ifc_cutter.leader_obj = (obj, obj.data) - elif "Stair" in obj.name: - ifc_cutter.stair_obj = obj - elif "Equal" in obj.name: - ifc_cutter.equal_objs.append(obj) - elif "Dimension" in obj.name: - ifc_cutter.dimension_objs.append(obj) - elif "Break" in obj.name: - ifc_cutter.break_obj = obj - elif "Hidden" in obj.name: - ifc_cutter.hidden_objs.append((obj, obj.data)) - elif "Solid" in obj.name: - ifc_cutter.solid_objs.append((obj, obj.data)) - elif "Plan Level" in obj.name: - ifc_cutter.plan_level_obj = obj - elif "Section Level" in obj.name: - ifc_cutter.section_level_obj = obj - elif obj.type == "FONT": - ifc_cutter.text_objs.append(obj) - else: - ifc_cutter.misc_objs.append(obj) - - ifc_cutter.section_box = { - "projection": tuple(projection), - "x_axis": tuple(x_axis), - "y_axis": tuple(y_axis), - "top_left_corner": tuple(top_left_corner), - "x": width, - "y": height, - "z": depth, - "shape": None, - "face": None, - } - ifc_cutter.cut_pickle_file = os.path.join(ifc_cutter.data_dir, "{}-cut.pickle".format(self.diagram_name)) - ifc_cutter.text_pickle_file = os.path.join(ifc_cutter.data_dir, "{}-text.pickle".format(self.diagram_name)) - ifc_cutter.metadata_pickle_file = os.path.join( - ifc_cutter.data_dir, "{}-metadata.pickle".format(self.diagram_name) - ) - ifc_cutter.should_recut = bpy.context.scene.DocProperties.should_recut - ifc_cutter.should_recut_selected = bpy.context.scene.DocProperties.should_recut_selected - selected_global_ids = [] - for obj in bpy.context.selected_objects: - if "Ifc" not in obj.name: - continue - for attribute in obj.BIMObjectProperties.attributes: - if attribute.name == "GlobalId": - selected_global_ids.append(attribute.string_value) - break - ifc_cutter.selected_global_ids = selected_global_ids - ifc_cutter.should_extract = bpy.context.scene.DocProperties.should_extract - svg_writer = svgwriter.SvgWriter(ifc_cutter) - if camera.data.BIMCameraProperties.diagram_scale == "CUSTOM": - human_scale, fraction = camera.data.BIMCameraProperties.custom_diagram_scale.split("|") - else: - human_scale, fraction = camera.data.BIMCameraProperties.diagram_scale.split("|") - numerator, denominator = fraction.split("/") - if camera.data.BIMCameraProperties.is_nts: - svg_writer.human_scale = "NTS" - else: - svg_writer.human_scale = human_scale - svg_writer.scale = float(numerator) / float(denominator) - ifc_cutter.cut() - svg_writer.write() - bpy.ops.bim.open_view(view=self.diagram_name) - return {"FINISHED"} - - def create_raster(self, camera, drawing_style): - if drawing_style.render_type == "NONE": - return - - if drawing_style.render_type == "DEFAULT": - return bpy.ops.render.render(write_still=True) - - previous_visibility = {} - for obj in camera.users_collection[0].objects: - previous_visibility[obj.name] = obj.hide_get() - obj.hide_set(True) - for obj in bpy.context.visible_objects: - if ( - (not obj.data and not obj.instance_collection) - or isinstance(obj.data, bpy.types.Camera) - or "IfcGrid/" in obj.name - or "IfcGridAxis/" in obj.name - or "IfcOpeningElement/" in obj.name - or self.does_obj_have_target_view_representation(obj, camera) - ): - previous_visibility[obj.name] = obj.hide_get() - obj.hide_set(True) - - space = self.get_view_3d() - previous_shading = space.shading.type - previous_format = bpy.context.scene.render.image_settings.file_format - space.shading.type = "RENDERED" - bpy.context.scene.render.image_settings.file_format = "PNG" - bpy.ops.render.opengl(write_still=True) - space.shading.type = previous_shading - bpy.context.scene.render.image_settings.file_format = previous_format - - for name, value in previous_visibility.items(): - bpy.data.objects[name].hide_set(value) - - def does_obj_have_target_view_representation(self, obj, camera): - if not obj.BIMObjectProperties.ifc_definition_id: - return False - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - if not element.Representation: - return False - for representation in element.Representation.Representations: - if ifcopenshell.util.representation.is_representation_of_context( - representation, "Plan", "Annotation", camera.data.BIMCameraProperties.target_view - ): - return True - - def is_landscape(self): - return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y - - def get_view_3d(self): - for area in bpy.context.screen.areas: - if area.type != "VIEW_3D": - continue - for space in area.spaces: - if space.type != "VIEW_3D": - continue - return space - - class AddSheet(bpy.types.Operator): bl_idname = "bim.add_sheet" bl_label = "Add Sheet"