diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 03715454e8..11db78bcf7 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -93,7 +93,7 @@ endif cp -r blenderbim/* dist/blenderbim/ # Provides IfcOpenShell Python functionality - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-$(PYNUMBER)-v0.7.0-93c5906-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-$(PYNUMBER)-v0.7.0-61e510e-$(PLATFORM)64.zip cd dist/working && unzip ifcblender* cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/ diff --git a/src/blenderbim/blenderbim/bim/data/styles/default.css b/src/blenderbim/blenderbim/bim/data/styles/default.css index 8cd89c06c2..4495940bd8 100644 --- a/src/blenderbim/blenderbim/bim/data/styles/default.css +++ b/src/blenderbim/blenderbim/bim/data/styles/default.css @@ -20,7 +20,7 @@ * { stroke-linecap: round; stroke-linejoin: round; } text { fill: black; stroke: none; } -.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; } +.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; } .annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .hidden { stroke-dasharray: 3, 2; } diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 5ec8d94944..16c924c24f 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -236,6 +236,7 @@ def setDefaultProperties(scene): ) ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner) ifcopenshell.api.owner.settings.get_application = get_application + # TODO: Move to drawing module if len(bpy.context.scene.DocProperties.drawing_styles) == 0: drawing_style = bpy.context.scene.DocProperties.drawing_styles.add() drawing_style.name = "Technical" @@ -289,7 +290,7 @@ def setDefaultProperties(scene): RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5, RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5), RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False, - RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: True, + RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: False, RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0, RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False, RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False, diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index e00ef1a5f6..3d9c6268d2 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -263,7 +263,7 @@ def get_active_drawing(scene): return None, None try: drawing = props.active_drawing - return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera + return drawing.camera.users_collection[0], drawing.camera except (KeyError, IndexError): raise RuntimeError("missing drawing collection") diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 7e20f657c5..7e62b6a31f 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -37,8 +37,9 @@ import blenderbim.bim.module.drawing.annotation as annotation 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 blenderbim.bim.module.drawing.prop import RasterStyleProperty +from lxml import etree from mathutils import Vector, Matrix, Euler, geometry +from blenderbim.bim.module.drawing.prop import RasterStyleProperty from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.group.data import Data as GroupData from ifcopenshell.api.pset.data import Data as PsetData @@ -280,38 +281,89 @@ class CreateDrawing(bpy.types.Operator): if os.path.isfile(svg_path) and self.props.should_use_linework_cache: return svg_path + # If we have already calculated it in the SVG in the past, don't recalculate + edited_guids = set() + for obj in IfcStore.edited_objs: + element = tool.Ifc.get_entity(obj) + edited_guids.add(element.GlobalId) if hasattr(element, "GlobalId") else None + cached_linework = set() + if os.path.isfile(svg_path): + tree = etree.parse(svg_path) + root = tree.getroot() + cached_linework = { + el.get("{http://www.ifcopenshell.org/ns}guid") + for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]") + } + cached_linework -= edited_guids + # This is a work in progress. See #1153 and #1564. + import hashlib import ifcopenshell.draw - files = [tool.Ifc.get()] - draw_settings = ifcopenshell.draw.draw_settings() - draw_settings.drawing_guid = self.camera_element.GlobalId - draw_settings.css = False - draw_settings.auto_floorplan = False - draw_settings.scale = self.scale + files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()} + + for ifc_path, ifc in files.items(): + # Don't use draw.main() just whilst we're prototyping and experimenting + ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest() + ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5") + + # Get all representation contexts to see what we're dealing with + # A drawing prioritises a target view context first, followed by a body context as a fallback + body_contexts = [] + target_view_contexts = [] + for rep_context in ifc.by_type("IfcGeometricRepresentationContext"): + if rep_context.is_a("IfcGeometricRepresentationSubContext"): + if rep_context.TargetView == context.scene.camera.data.BIMCameraProperties.target_view: + target_view_contexts.append(rep_context.id()) + continue + if rep_context.ContextIdentifier in ["Body", "Facetation"]: + body_contexts.append(rep_context.id()) + continue + if rep_context.is_a() == "IfcGeometricRepresentationContext" and rep_context.ContextType == "Model": + body_contexts.append(rep_context.id()) + continue + + elements = set(ifc.by_type("IfcElement")) - set(ifc.by_type("IfcOpeningElement")) + # elements = set([tool.Ifc.get_entity(o) for o in bpy.context.visible_objects if tool.Ifc.get_entity(o).is_a("IfcProduct")]) + + self.setup_serialiser(ifc) + cache_settings = ifcopenshell.geom.settings( + APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True, SEW_SHELLS=True + ) + cache = ifcopenshell.geom.serializers.hdf5(ifc_cache_path, cache_settings) + + if target_view_contexts: + geom_settings = ifcopenshell.geom.settings( + APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True, SEW_SHELLS=True, INCLUDE_CURVES=True + ) + geom_settings.set_context_ids(target_view_contexts) + it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements) + it.set_cache(cache) + processed = set() + for elem in self.yield_from_iterator(it): + processed.add(ifc.by_guid(elem.guid)) + self.serialiser.write(elem) + elements -= processed - iterators = [] - for f in files: - geom_settings = ifcopenshell.geom.settings(APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True) - body_contexts = [ - c.id() - for c in self.file.by_type("IfcGeometricRepresentationSubContext") - if c.ContextIdentifier in ["Body", "Facetation"] - ] if body_contexts: + geom_settings = ifcopenshell.geom.settings( + APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True, SEW_SHELLS=True + ) geom_settings.set_context_ids(body_contexts) - exclude = f.by_type("IfcOpeningElement") - if self.camera.data.BIMCameraProperties.target_view == "PLAN_VIEW": - # For plans, we exclude windows and doors by default - exclude += f.by_type("IfcWindow") + f.by_type("IfcDoor") - # Exclude all annotation apart from the active one so we don't cut through them - exclude += [e for e in f.by_type("IfcAnnotation") if e != self.camera_element] + it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements) + it.set_cache(cache) + for elem in self.yield_from_iterator(it): + self.serialiser.write(elem) - iterators.append(ifcopenshell.geom.iterator(geom_settings, f, multiprocessing.cpu_count(), exclude=exclude)) + geom_settings = ifcopenshell.geom.settings( + APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True, SEW_SHELLS=True + ) + it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element]) + for elem in self.yield_from_iterator(it): + self.serialiser.write(elem) - results = ifcopenshell.draw.main(draw_settings, files, iterators=iterators, merge_projection=False) - - from lxml import etree + self.serialiser.finalize() + results = self.svg_buffer.get_value() root = etree.fromstring(results) self.enrich_linework_with_metadata(root) @@ -321,26 +373,57 @@ class CreateDrawing(bpy.types.Operator): return svg_path + def setup_serialiser(self, ifc): + self.svg_settings = ifcopenshell.geom.settings( + APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True, INCLUDE_CURVES=True + ) + self.svg_buffer = ifcopenshell.geom.serializers.buffer() + self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings) + self.serialiser.setFile(ifc) + self.serialiser.setWithoutStoreys(True) + self.serialiser.setPolygonal(True) + self.serialiser.setUseNamespace(True) + self.serialiser.setAlwaysProject(True) + self.serialiser.setAutoElevation(False) + self.serialiser.setAutoSection(False) + self.serialiser.setPrintSpaceNames(False) + self.serialiser.setPrintSpaceAreas(False) + self.serialiser.setDrawDoorArcs(False) + self.serialiser.setNoCSS(True) + self.serialiser.setElevationRefGuid(self.camera_element.GlobalId) + self.serialiser.setScale(self.scale) + self.serialiser.setSubtractionSettings(ifcopenshell.ifcopenshell_wrapper.ALWAYS) + # tree = ifcopenshell.geom.tree() + # This instructs the tree to explode BReps into faces and return + # the style of the face when running tree.select_ray() + # tree.enable_face_styles(True) + + def yield_from_iterator(self, it): + if it.initialize(): + while True: + yield it.get() + if not it.next(): + break + def enrich_linework_with_metadata(self, root): ifc = tool.Ifc.get() - for el in root.findall('.//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]'): - classes = [] + for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): + classes = ["cut"] element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid")) material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) if material: if material.is_a("IfcMaterialLayerSet"): name = material.LayerSetName or "null" else: - name = getattr(material, "Name", "null") + name = getattr(material, "Name", "null") or "null" name = self.canonicalise_class_name(name) classes.append(f"material-{name}") - if classes: - el.set("class", (el.get("class", "") + " " + " ".join(classes)).strip()) + el.set("class", (el.get("class", "") + " " + " ".join(classes)).strip()) def move_projection_to_bottom(self, root): # https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value - group = root.find('{http://www.w3.org/2000/svg}g') - #group[:] = sorted(group, key=lambda e : "projection" in e.get("class")) + group = root.find("{http://www.w3.org/2000/svg}g") + # group[:] = sorted(group, key=lambda e : "projection" in e.get("class")) group[:] = reversed(group) def canonicalise_class_name(self, name): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index d88efdc7b2..2d05c14520 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -337,13 +337,15 @@ class BIMCameraProperties(PropertyGroup): # For now, this JSON dump are all the parameters that determine a camera's "Block representation" # By checking this, you will know whether or not the camera IFC representation needs to be refreshed def update_representation(self, obj): - representation = json.dumps({ - "matrix": [list(x) for x in obj.matrix_world], - "raster_x": self.raster_x, - "raster_y": self.raster_y, - "ortho_scale": obj.data.ortho_scale, - "clip_end": obj.data.clip_end, - }) + representation = json.dumps( + { + "matrix": [list(x) for x in obj.matrix_world], + "raster_x": self.raster_x, + "raster_y": self.raster_y, + "ortho_scale": obj.data.ortho_scale, + "clip_end": obj.data.clip_end, + } + ) if self.representation != representation: self.representation = representation return True