diff --git a/src/ifcblenderexport/blenderbim/__init__.py b/src/ifcblenderexport/blenderbim/__init__.py index dea12bc80a..da448a7b07 100644 --- a/src/ifcblenderexport/blenderbim/__init__.py +++ b/src/ifcblenderexport/blenderbim/__init__.py @@ -61,8 +61,10 @@ classes = ( operator.FetchObjectPassport, operator.AddSubcontext, operator.RemoveSubcontext, + operator.CutSection, prop.Subcontext, prop.BIMProperties, + prop.DocProperties, prop.BIMLibrary, prop.MapConversion, prop.TargetCRS, @@ -75,6 +77,7 @@ classes = ( prop.BIMMaterialProperties, prop.SweptSolid, prop.BIMMeshProperties, + ui.BIM_PT_documentation, ui.BIM_PT_bim, ui.BIM_PT_context, ui.BIM_PT_qa, @@ -102,6 +105,7 @@ def register(): bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) + bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties) bpy.types.Scene.BIMLibrary = bpy.props.PointerProperty(type=prop.BIMLibrary) bpy.types.Scene.MapConversion = bpy.props.PointerProperty(type=prop.MapConversion) bpy.types.Scene.TargetCRS = bpy.props.PointerProperty(type=prop.TargetCRS) @@ -117,6 +121,7 @@ def unregister(): bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) del(bpy.types.Scene.BIMProperties) + del(bpy.types.Scene.DocProperties) del(bpy.types.Scene.MapConversion) del(bpy.types.Scene.TargetCRS) del(bpy.types.Object.BIMObjectProperties) diff --git a/src/ifcblenderexport/blenderbim/cut_ifc.py b/src/ifcblenderexport/blenderbim/cut_ifc.py index 3604a11966..c4ad1f47a6 100644 --- a/src/ifcblenderexport/blenderbim/cut_ifc.py +++ b/src/ifcblenderexport/blenderbim/cut_ifc.py @@ -1,11 +1,19 @@ import os import math -import svgwrite import time import numpy import pickle +import sys from pathlib import Path +from mathutils import Vector +import xml.etree.ElementTree as ET +# This hack is required to get the svgwrite dependency to load +# TODO: decouple cutter into its own binary +cwd = os.path.dirname(os.path.realpath(__file__)) + os.path.sep +sys.path.append(cwd) + +import svgwrite import OCC.gp import OCC.Geom import OCC.Bnd @@ -43,11 +51,14 @@ class IfcCutter: self.product_shapes = [] self.background_elements = [] self.cut_polygons = [] + self.data_dir = '' self.ifc_files = [] self.unit = None self.resolved_pixels = set() self.should_get_background = False self.pickle_file = 'shapes.pickle' + self.diagram_name = None + self.background_image = None self.section_box = { 'projection': (0, 1, 0), 'x_axis': (1, 0, 0), @@ -103,7 +114,7 @@ class IfcCutter: print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time)) def load_ifc_files(self): - for filename in Path('ifc/').glob('*.ifc'): + for filename in Path(self.data_dir).glob('*.ifc'): print('Loading file {} ...'.format(filename)) self.ifc_files.append(ifcopenshell.open(filename)) @@ -558,6 +569,24 @@ class IfcCutterDebug(IfcCutter): ifcopenshell.geom.utils.display_shape(element['geometry_face']) input('Debug: showing background elements.') + +class External(svgwrite.container.Group): + def __init__(self, xml, **extra): + self.xml = xml + + # Remove namespace + ns = u'{http://www.w3.org/2000/svg}' + nsl = len(ns) + for elem in self.xml.getiterator(): + if elem.tag.startswith(ns): + elem.tag = elem.tag[nsl:] + + super(External, self).__init__(**extra) + + def get_xml(self): + return self.xml + + class SvgWriter(): def __init__(self, ifc_cutter): self.ifc_cutter = ifc_cutter @@ -565,27 +594,52 @@ class SvgWriter(): def write(self): self.calculate_scale() - self.svg = svgwrite.Drawing('output.svg', + self.output = os.path.join( + self.ifc_cutter.data_dir, + 'diagrams', + self.ifc_cutter.diagram_name + '.svg' + ) + self.svg = svgwrite.Drawing( + self.output, debug=False, - size=('{}mm'.format(self.ifc_cutter.section_box['x'] * self.scale), - '{}mm'.format(self.ifc_cutter.section_box['y'] * self.scale)), - viewBox=('0 0 {} {}'.format( - self.ifc_cutter.section_box['x'] * self.scale, - self.ifc_cutter.section_box['y'] * self.scale))) + size=('{}mm'.format(self.width), '{}mm'.format(self.height)), + viewBox=('0 0 {} {}'.format(self.width, self.height))) + self.add_stylesheet() + self.add_defs() + self.draw_background_image() self.draw_background_elements() self.draw_cut_polygons() + self.draw_annotations() self.svg.save(pretty=True) def calculate_scale(self): # TODO: properly handle units if self.ifc_cutter.unit.Name == 'METRE': self.scale *= 1000 + self.raw_width = self.ifc_cutter.section_box['x'] + self.raw_height = self.ifc_cutter.section_box['y'] + self.width = self.raw_width * self.scale + self.height = self.raw_height * self.scale def add_stylesheet(self): - with open('styles/default.css', 'r') as stylesheet: + with open('{}styles/default.css'.format(self.ifc_cutter.data_dir), 'r') as stylesheet: self.svg.defs.add(self.svg.style(stylesheet.read())) + def add_defs(self): + tree = ET.parse('{}styles/defs.svg'.format(self.ifc_cutter.data_dir)) + root = tree.getroot() + for child in root.getchildren(): + self.svg.defs.add(External(child)) + + def draw_background_image(self): + self.svg.add(self.svg.image( + os.path.basename(self.ifc_cutter.background_image), **{ + 'width': self.width, + 'height': self.height + } + )) + def draw_background_elements(self): for element in self.ifc_cutter.background_elements: if element['type'] == 'polygon': @@ -595,6 +649,19 @@ class SvgWriter(): elif element['type'] == 'line': self.draw_line(element, 'background') + def draw_annotations(self): + x_offset = self.raw_width / 2 + y_offset = self.raw_height / 2 + for edge in self.ifc_cutter.annotation_obj.data.edges: + classes = ['annotation', 'dimension'] + v0 = self.ifc_cutter.annotation_obj.data.vertices[edge.vertices[0]].co + v1 = self.ifc_cutter.annotation_obj.data.vertices[edge.vertices[1]].co + start = ((x_offset + v0.x) * self.scale, (y_offset - v0.y) * self.scale) + end = ((x_offset + v1.x) * self.scale, (y_offset - v1.y) * self.scale) + line = self.svg.add(self.svg.line(start=start, end=end, class_=' '.join(classes))) + line['marker-start'] = 'url(#dimension-marker)' + line['marker-end'] = 'url(#dimension-marker)' + def draw_cut_polygons(self): for polygon in self.ifc_cutter.cut_polygons: self.draw_polygon(polygon, 'cut') @@ -636,12 +703,3 @@ class SvgWriter(): classes.append('material-{}'.format(association.RelatingMaterial.Name)) classes.append('globalid-{}'.format(element.GlobalId)) return classes - -print('# Starting process') -ifc_cutter = IfcCutter() -#ifc_cutter = IfcCutterDebug() -svg_writer = SvgWriter(ifc_cutter) -ifc_cutter.cut() -start_time = time.time() -svg_writer.write() -print('# SVG writing finished in {:.2f} seconds'.format(time.time() - start_time)) diff --git a/src/ifcblenderexport/blenderbim/data/diagrams/.gitkeep b/src/ifcblenderexport/blenderbim/data/diagrams/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ifcblenderexport/blenderbim/data/styles/default.css b/src/ifcblenderexport/blenderbim/data/styles/default.css new file mode 100644 index 0000000000..9759ec682e --- /dev/null +++ b/src/ifcblenderexport/blenderbim/data/styles/default.css @@ -0,0 +1,3 @@ +.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3px; } +.background { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.05px; } +.annotation { stroke: black; stroke-linecap: 'round'; stroke-width: 0.05px; } diff --git a/src/ifcblenderexport/blenderbim/data/styles/defs.svg b/src/ifcblenderexport/blenderbim/data/styles/defs.svg new file mode 100644 index 0000000000..2f90264534 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/data/styles/defs.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/ifcblenderexport/blenderbim/operator.py b/src/ifcblenderexport/blenderbim/operator.py index 003ec1d4bd..ea56e93ebb 100644 --- a/src/ifcblenderexport/blenderbim/operator.py +++ b/src/ifcblenderexport/blenderbim/operator.py @@ -6,8 +6,10 @@ import logging from . import ifcopenshell from . import export_ifc from . import import_ifc +from . import cut_ifc from bpy_extras.io_utils import ImportHelper from itertools import cycle +from mathutils import Vector class ExportIFC(bpy.types.Operator): bl_idname = "export.ifc" @@ -751,3 +753,59 @@ class RemoveSubcontext(bpy.types.Operator): subcontext_index = int(subcontext_index) getattr(bpy.context.scene.BIMProperties, '{}_subcontexts'.format(context)).remove(subcontext_index) return {'FINISHED'} + + +class CutSection(bpy.types.Operator): + bl_idname = 'bim.cut_section' + bl_label = 'Cut Section' + + def execute(self, context): + camera = bpy.context.active_object + if not (camera.type == 'CAMERA' and camera.data.type == 'ORTHO'): + return {'FINISHED'} + self.diagram_name = camera.name + bpy.context.scene.render.filepath = os.path.join( + bpy.context.scene.BIMProperties.data_dir, + 'diagrams', + '{}.png'.format(self.diagram_name) + ) + bpy.ops.render.render(write_still=True) + 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.data_dir = bpy.context.scene.BIMProperties.data_dir + ifc_cutter.diagram_name = self.diagram_name + ifc_cutter.background_image = bpy.context.scene.render.filepath + ifc_cutter.annotation_obj = bpy.data.objects['{}-Annotation'.format( + self.diagram_name + )] + 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.pickle_file = os.path.join(ifc_cutter.data_dir, 'shapes.pickle') + svg_writer = cut_ifc.SvgWriter(ifc_cutter) + ifc_cutter.cut() + svg_writer.write() + return {'FINISHED'} + + def is_landscape(self): + return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y diff --git a/src/ifcblenderexport/blenderbim/prop.py b/src/ifcblenderexport/blenderbim/prop.py index 22f8ea4d33..ac3d54fc80 100644 --- a/src/ifcblenderexport/blenderbim/prop.py +++ b/src/ifcblenderexport/blenderbim/prop.py @@ -171,6 +171,10 @@ class Subcontext(PropertyGroup): target_view: StringProperty(name='Target View') +class DocProperties(PropertyGroup): + blah: StringProperty(name="Blah") + + class BIMProperties(PropertyGroup): schema_dir: StringProperty(default=os.path.join(cwd ,'schema') + os.path.sep, name="Schema Directory") data_dir: StringProperty(default=os.path.join(cwd, 'data') + os.path.sep, name="Data Directory") diff --git a/src/ifcblenderexport/blenderbim/ui.py b/src/ifcblenderexport/blenderbim/ui.py index 7f990fbb34..ddad30abec 100644 --- a/src/ifcblenderexport/blenderbim/ui.py +++ b/src/ifcblenderexport/blenderbim/ui.py @@ -182,6 +182,20 @@ class BIM_PT_gis(Panel): layout.row().prop(scene.TargetCRS, 'map_unit') +class BIM_PT_documentation(Panel): + bl_label = "Diagrammatic Documentation" + bl_idname = "BIM_PT_documentation" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'output' + + def draw(self, context): + layout = self.layout + props = bpy.context.scene.DocProperties + + row = layout.row() + row.operator('bim.cut_section') + class BIM_PT_context(Panel): bl_label = "Geometric Representation Contexts" bl_idname = "BIM_PT_context"