From 2d42846fa395e678390e4897d3f3998abc3647e1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 17 May 2021 21:52:23 +1000 Subject: [PATCH] Basic reimplementation of the 2D representation support without PythonOCC. See #1153. --- .../blenderbim/bim/data/styles/default.css | 6 +- src/blenderbim/blenderbim/bim/helper.py | 9 -- src/blenderbim/blenderbim/bim/import_ifc.py | 7 +- .../blenderbim/bim/module/drawing/helper.py | 106 ++++++++++++++++++ .../blenderbim/bim/module/drawing/operator.py | 105 ++++++++++++++++- .../blenderbim/bim/module/drawing/prop.py | 6 +- .../blenderbim/bim/module/drawing/ui.py | 3 +- .../blenderbim/bim/module/group/ui.py | 2 +- 8 files changed, 221 insertions(+), 23 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/data/styles/default.css b/src/blenderbim/blenderbim/bim/data/styles/default.css index 920ad5903c..133c191ce1 100644 --- a/src/blenderbim/blenderbim/bim/data/styles/default.css +++ b/src/blenderbim/blenderbim/bim/data/styles/default.css @@ -1,6 +1,6 @@ -* { stroke-linecap: round; } -.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; } -.background { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } +* { stroke-linecap: round; stroke-linejoin: round; } +*[id] { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; } +.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .hidden { stroke-dasharray: 3, 2; } .solid {} diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 4d7666e9eb..6bb3ead78b 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -88,15 +88,6 @@ def parse_diagram_scale(camera): return float(numerator) / float(denominator) -def get_project_collection(scene): - """Get main project collection""" - - colls = [c for c in scene.collection.children if c.name.startswith("IfcProject")] - if len(colls) != 1: - raise RuntimeError("project collection missing or not unique") - return colls[0] - - def ortho_view_frame(camera, margin=0.015): """Calculates 2d bounding box of camera view area. diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index c4b245629b..75972d2713 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -18,7 +18,7 @@ import multiprocessing import zipfile import tempfile import numpy as np -import blenderbim.bim.prop +from blenderbim.bim.module.drawing.prop import getDiagramScales from pathlib import Path from itertools import cycle from datetime import datetime @@ -1218,7 +1218,8 @@ class IfcImporter: if not view_collection: view_collection = bpy.data.collections.new("Views") bpy.context.scene.collection.children.link(view_collection) - drawing_collection = bpy.data.collections.new(obj.name) + group = [r for r in element.HasAssignments if r.is_a("IfcRelAssignsToGroup")][0].RelatingGroup + drawing_collection = bpy.data.collections.new("IfcGroup/" + group.Name) view_collection.children.link(drawing_collection) drawing_collection.objects.link(obj) else: @@ -1342,7 +1343,7 @@ class IfcImporter: camera.BIMCameraProperties.target_view = pset["TargetView"] if "Scale" in pset: valid_scales = [ - i[0] for i in blenderbim.bim.prop.getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1] + i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1] ] if valid_scales: camera.BIMCameraProperties.diagram_scale = valid_scales[0] diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index 8f299add09..a71e549b23 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -1,5 +1,102 @@ import bpy import math +import mathutils.geometry +from mathutils import Vector + +# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2 + + +class BoundingEdge: + def __init__(self, v0, v1): + self.vertex = (v0, v1) + self.vector = v1 - v0 + + +class BoundingFace: + def __init__(self, v0, v1, v2): + self.vertex = (v0, v1, v2) + self.normal = mathutils.geometry.normal(v0, v1, v2) + + +class BoundingBox: + def __init__(self, ob, vertex=None): + self.vertex = vertex or [ob.matrix_world @ Vector(v) for v in ob.bound_box] + if self.vertex != None: + self.edge = [ + BoundingEdge(self.vertex[0], self.vertex[1]), + BoundingEdge(self.vertex[1], self.vertex[2]), + BoundingEdge(self.vertex[2], self.vertex[3]), + BoundingEdge(self.vertex[3], self.vertex[0]), + BoundingEdge(self.vertex[4], self.vertex[5]), + BoundingEdge(self.vertex[5], self.vertex[6]), + BoundingEdge(self.vertex[6], self.vertex[7]), + BoundingEdge(self.vertex[7], self.vertex[4]), + BoundingEdge(self.vertex[0], self.vertex[4]), + BoundingEdge(self.vertex[1], self.vertex[5]), + BoundingEdge(self.vertex[2], self.vertex[6]), + BoundingEdge(self.vertex[3], self.vertex[7]), + ] + self.face = [ + BoundingFace(self.vertex[0], self.vertex[1], self.vertex[3]), + BoundingFace(self.vertex[0], self.vertex[4], self.vertex[1]), + BoundingFace(self.vertex[0], self.vertex[3], self.vertex[4]), + BoundingFace(self.vertex[6], self.vertex[5], self.vertex[7]), + BoundingFace(self.vertex[6], self.vertex[7], self.vertex[2]), + BoundingFace(self.vertex[6], self.vertex[2], self.vertex[5]), + ] + + def whichSide(self, vtxs, normal, faceVtx): + retVal = 0 + positive = 0 + negative = 0 + for v in vtxs: + t = normal.dot(v - faceVtx) + if t > 0: + positive = positive + 1 + elif t < 0: + negative = negative + 1 + + if positive != 0 and negative != 0: + return 0 + + if positive != 0: + retVal = 1 + else: + retVal = -1 + return retVal + + # Taken from: http://www.geometrictools.com/Documentation/MethodOfSeparatingAxes.pdf + def intersect(self, bb): + retVal = False + if self.vertex != None and bb.vertex != None: + # check all the faces of this object for a seperation axis + for i, f in enumerate(self.face): + d = f.normal + if self.whichSide(bb.vertex, d, f.vertex[0]) > 0: + return False # all the vertexes are on the +ve side of the face + + # now do it again for the other objects faces + for i, f in enumerate(bb.face): + d = f.normal + if self.whichSide(self.vertex, d, f.vertex[0]) > 0: + return False # all the vertexes are on the +ve side of the face + + # do edge checks + for e1 in self.edge: + for e2 in bb.edge: + d = e1.vector.cross(e2.vector) + side0 = self.whichSide(self.vertex, d, e1.vertex[0]) + if side0 == 0: + continue + side1 = self.whichSide(bb.vertex, d, e1.vertex[0]) + if side1 == 0: + continue + + if (side0 * side1) < 0: + return False + + retVal = True + return retVal # This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py @@ -151,3 +248,12 @@ def get_active_drawing(scene): return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera except (KeyError, IndexError): raise RuntimeError("missing drawing collection") + + +def get_project_collection(scene): + """Get main project collection""" + + colls = [c for c in scene.collection.children if c.name.startswith("IfcProject")] + if len(colls) != 1: + raise RuntimeError("project collection missing or not unique") + return colls[0] diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 58b0258ac6..e3d968bafd 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -1,6 +1,8 @@ import os +import re import bpy import json +import time import bmesh import subprocess import webbrowser @@ -8,9 +10,10 @@ 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.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 from mathutils import Vector, Matrix, Euler, geometry from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.group.data import Data as GroupData @@ -33,6 +36,7 @@ class AddDrawing(bpy.types.Operator): bl_label = "Add Drawing" def execute(self, context): + self.file = IfcStore.get_file() new = context.scene.DocProperties.drawings.add() new.name = "DRAWING {}".format(len(context.scene.DocProperties.drawings)) if not bpy.data.collections.get("Views"): @@ -58,7 +62,9 @@ class AddDrawing(bpy.types.Operator): bpy.ops.bim.activate_drawing_style() bpy.ops.bim.add_group() - bpy.ops.bim.assign_group(product=camera.name, group=sorted(GroupData.groups.keys())[-1]) + group = self.file.by_id(sorted(GroupData.groups.keys())[-1]) + ifcopenshell.api.run("group.edit_group", self.file, **{"group": group, "attributes": {"Name": new.name}}) + bpy.ops.bim.assign_group(product=camera.name, group=group.id()) bpy.ops.bim.add_pset(obj=camera.name, obj_type="Object", pset_name="EPset_Drawing") pset_id = sorted(PsetData.products[camera.BIMObjectProperties.ifc_definition_id]["psets"])[-1] bpy.ops.bim.edit_pset( @@ -81,22 +87,45 @@ class CreateDrawing(bpy.types.Operator): or not self.camera.BIMObjectProperties.ifc_definition_id ): return + self.file = IfcStore.get_file() + self.time = None + start = time.time() + self.profile_code("Start drawing generation process") self.props = context.scene.DocProperties self.drawing_name = IfcStore.get_file().by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name base_svg = self.ifc_to_svg(context) + self.profile_code("Generate base layer") annotation_svg = self.annotation_to_svg(context) + self.profile_code("Generate annotation layer") svg_path = self.combine_svgs(context, base_svg, annotation_svg) + self.profile_code("Combine SVG layers") open_with_user_command(bpy.context.preferences.addons["blenderbim"].preferences.svg_command, svg_path) + print("Total Time: {:.2f}".format(time.time() - start)) return {"FINISHED"} + 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 combine_svgs(self, context, base, annotation): # Hacky :) svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg") with open(svg_path, "w") as outfile: with open(base) as infile: + should_skip = False for line in infile: if "" in line: continue + elif "" in line: + should_skip = True + continue + elif "" in line: + should_skip = False + continue + elif should_skip: + continue outfile.write(line) with open(annotation) as infile: for i, line in enumerate(infile): @@ -127,6 +156,8 @@ class CreateDrawing(bpy.types.Operator): "entities", "IfcSpace", "IfcOpeningElement", + "IfcDoor", + "IfcWindow", ] ) return svg_path @@ -205,6 +236,7 @@ class CreateDrawing(bpy.types.Operator): svg_writer.annotations.setdefault("misc_objs", []).append(obj) svg_writer.annotations["attributes"] = [a.name for a in drawing_style.attributes] + svg_writer.annotations["annotation_objs"] = self.get_annotation(svg_writer) svg_writer.write() return svg_writer.output @@ -212,6 +244,75 @@ class CreateDrawing(bpy.types.Operator): def is_landscape(self): return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y + def get_annotation(self, svg_writer): + results = [] + x = svg_writer.camera_width / 2 + y = svg_writer.camera_height / 2 + z = 0.01 + camera_box = helper.BoundingBox( + self.camera, + [ + self.camera.matrix_world @ Vector((-x, -y, -z)), + self.camera.matrix_world @ Vector((-x, -y, 0)), + self.camera.matrix_world @ Vector((-x, y, 0)), + self.camera.matrix_world @ Vector((-x, y, -z)), + self.camera.matrix_world @ Vector((x, -y, -z)), + self.camera.matrix_world @ Vector((x, -y, 0)), + self.camera.matrix_world @ Vector((x, y, 0)), + self.camera.matrix_world @ Vector((x, y, -z)), + ], + ) + # This should probably be also part of IfcConvert in the future, here is a Python prototype + settings_2d = ifcopenshell.geom.settings() + settings_2d.set(settings_2d.INCLUDE_CURVES, True) + for obj in bpy.data.objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + if obj == self.camera: + continue + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + representation = ifcopenshell.util.representation.get_representation( + element, "Plan", "Annotation", self.camera.data.BIMCameraProperties.target_view + ) + if not representation: + continue + if not camera_box.intersect(helper.BoundingBox(obj)): + continue + shape = ifcopenshell.geom.create_shape(settings_2d, representation) + geometry = shape + e = geometry.edges + v = geometry.verts + results.append( + { + "raw": element, + "classes": self.get_classes(element, "annotation", svg_writer), + "edges": [[e[i], e[i + 1]] for i in range(0, len(e), 2)], + "vertices": [obj.matrix_world @ Vector((v[i], v[i + 1], v[i + 2])) for i in range(0, len(v), 3)], + } + ) + return results + + def get_classes(self, element, position, svg_writer): + 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 svg_writer.annotations["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()) + class AddAnnotation(bpy.types.Operator): bl_idname = "bim.add_annotation" diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index 35952dfc37..0cae4c6190 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -38,10 +38,10 @@ def getDiagramScales(self, context): global diagram_scales_enum if ( len(diagram_scales_enum) < 1 - or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13) - or (context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31) + or (bpy.context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13) + or (bpy.context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31) ): - if context.scene.unit_settings.system == "IMPERIAL": + if bpy.context.scene.unit_settings.system == "IMPERIAL": diagram_scales_enum = [ ("CUSTOM", "Custom", ""), ("1'=1'-0\"|1/1", "1'=1'-0\"", ""), diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index ba7e82ea94..38d3c8f745 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -106,8 +106,7 @@ class BIM_PT_camera(Panel): row.operator("bim.activate_drawing_style") row = layout.row(align=True) - row.operator("bim.cut_section", text="Create Drawing") - row.operator("bim.create_drawing", text="Create Drawing 2.0") + row.operator("bim.create_drawing", text="Create Drawing") op = row.operator("bim.open_view", icon="URL", text="") op.view = context.active_object.name.split("/")[1] diff --git a/src/blenderbim/blenderbim/bim/module/group/ui.py b/src/blenderbim/blenderbim/bim/module/group/ui.py index a0f1a09125..21cd9a9870 100644 --- a/src/blenderbim/blenderbim/bim/module/group/ui.py +++ b/src/blenderbim/blenderbim/bim/module/group/ui.py @@ -24,7 +24,7 @@ class BIM_PT_groups(Panel): row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER") if self.props.is_editing: row.operator("bim.add_group", text="", icon="ADD") - row.operator("bim.disable_group_editing_ui", text="", icon="CHECKMARK") + row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL") else: row.operator("bim.load_groups", text="", icon="GREASEPENCIL")