# Bonsai - OpenBIM Blender Add-on # Copyright (C) 2020, 2021 Dion Moult # # This file is part of Bonsai. # # Bonsai is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Bonsai is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . import bpy import uuid import math import sys # sys.path.append('C:\Program Files\Python37\Lib\site-packages') import lxml import lxml.etree import bspy # pyright: ignore[reportMissingImports] from bspy import Gbxml # pyright: ignore[reportMissingImports] class GbxmlExporter: def __init__(self): self.gbxml = Gbxml() self.campus = None def export(self): print("# Start export") self.campus = self.gbxml.add_element(self.gbxml.root(), "Campus") self.campus.set("id", "campus-1") name = self.gbxml.add_element(self.campus, "Name", "My project") location = self.gbxml.add_element(self.campus, "Location") self.gbxml.add_element(location, "ZipcodeOrPostalCode", "G20 0SP") self.gbxml.add_element(location, "Name", "London/Heathrow") self.gbxml.add_element(location, "Latitude", "51.480000") self.gbxml.add_element(location, "Longitude", "-0.450000") self.gbxml.add_element(location, "Elevation", "24.000000") building = self.gbxml.add_element(self.campus, "Building") building.set("id", str(uuid.uuid4())) building.set("buildingType", "Office") for object in bpy.context.selected_objects: self.create_space(object, building) # hardcoded test construction = self.gbxml.add_element(self.gbxml.root(), "Construction") construction.set("id", "defaultconstruction") self.gbxml.add_element(construction, "Name", "test construction name") u_value = self.gbxml.add_element(construction, "U-value", "0.42") u_value.set("unit", "WPerSquareMeterK") layer = self.gbxml.add_element(construction, "LayerId") layer.set("layerIdRef", "defaultlayer") layer = self.gbxml.add_element(self.gbxml.root(), "Layer") layer.set("id", "defaultlayer") material = self.gbxml.add_element(layer, "MaterialId") material.set("materialIdRef", "defaultmaterial") material = self.gbxml.add_element(self.gbxml.root(), "Material") material.set("id", "defaultmaterial") thickness = self.gbxml.add_element(material, "Thickness", "0.2") thickness.set("unit", "Meters") self.gbxml.add_element(material, "Name", "test material name") r_value = self.gbxml.add_element(material, "R-value", "0.13") r_value.set("unit", "SquareMeterKPerW") self.append_template("C:/cygwin64/home/moud308/Projects/New Folder/presets/light-schedule.xml") self.append_template("C:/cygwin64/home/moud308/Projects/New Folder/presets/window-types.xml") with open("C:/cygwin64/home/moud308/Projects/New Folder/out.xml", "w") as out: out.write(self.gbxml.xmlstring()) print("# Validation results: {}".format(self.gbxml.validate())) print("# Finish export") def append_template(self, file): parser = lxml.etree.XMLParser(remove_blank_text=True) template = lxml.etree.parse(file, parser).findall(".")[0] for child in template.getchildren(): self.gbxml.root().append(child) def create_space(self, object, building): space = self.gbxml.add_element(building, "Space") space.set("id", object.name) space.set("lightScheduleIdRef", "aim0130") # hardcoded self.gbxml.add_element(space, "Name", object.name) light_power_per_area = self.gbxml.add_element(space, "LightPowerPerArea", "5") # hardcoded test light_power_per_area.set("unit", "WattPerSquareMeter") calculated_area = 0 shell_geometry = self.gbxml.add_element(space, "ShellGeometry") shell_geometry.set("id", "shellid") closed_shell = self.gbxml.add_element(shell_geometry, "ClosedShell") vertices_in_vg = self.get_vertices_in_vg(object, 0) for polygon in object.data.polygons: # First vg is reserved for surfaces if object.vertex_groups and not self.is_polygon_in_vg(polygon, vertices_in_vg): continue calculated_area += polygon.area self.create_poly_loop(object, polygon, closed_shell) self.create_space_boundary(object, polygon, space) self.create_surface(object, polygon) self.gbxml.add_element(space, "Area", str(calculated_area)) self.gbxml.add_element(space, "Volume", str(self.get_volume(object))) def get_vertices_in_vg(self, object, vg_index): return [v.index for v in object.data.vertices if vg_index in [g.group for g in v.groups]] # Can move into a common Blender helper class? def is_polygon_in_vg(self, polygon, vertices_in_vg): for v in polygon.vertices: if v not in vertices_in_vg: return False return True def create_space_boundary(self, object, polygon, parent): space_boundary = self.gbxml.add_element(parent, "SpaceBoundary") space_boundary.set("isSecondLevelBoundary", "true") space_boundary.set("surfaceIdRef", "surface-{}-{}".format(object.name, polygon.index)) planar_geometry = self.gbxml.add_element(space_boundary, "PlanarGeometry") self.create_poly_loop(object, polygon, planar_geometry) def create_surface(self, object, polygon): surface = self.gbxml.add_element(self.campus, "Surface") surface.set("id", "surface-{}-{}".format(object.name, polygon.index)) surface.set("surfaceType", "ExteriorWall") surface.set("constructionIdRef", "defaultconstruction") adjacent_space_id = self.gbxml.add_element(surface, "AdjacentSpaceId") adjacent_space_id.set("spaceIdRef", object.name) rectangular_geometry = self.gbxml.add_element(surface, "RectangularGeometry") self.gbxml.add_element( rectangular_geometry, "Azimuth", str(math.degrees(math.atan2(polygon.normal[0], polygon.normal[1]))) ) self.gbxml.add_element( rectangular_geometry, "Tilt", str(math.degrees(math.atan2(polygon.normal[2], polygon.normal[1])) - 90) ) planar_geometry = self.gbxml.add_element(surface, "PlanarGeometry") self.create_poly_loop(object, polygon, planar_geometry) for vg in object.vertex_groups: if "/".join(vg.name.split("/")[0:2]) == "openings/{}".format(polygon.index): vertices_in_vg = self.get_vertices_in_vg(object, vg.index) for p in object.data.polygons: if self.is_polygon_in_vg(p, vertices_in_vg): self.create_opening(object, p, surface) def create_opening(self, object, polygon, parent): opening = self.gbxml.add_element(parent, "Opening") opening.set("id", "opening-{}-{}".format(object.name, polygon.index)) opening.set("windowTypeIdRef", "STD_EX11") # hardcoded opening.set("openingType", "FixedWindow") # hardcoded planar_geometry = self.gbxml.add_element(opening, "PlanarGeometry") self.create_poly_loop(object, polygon, planar_geometry) def create_poly_loop(self, object, polygon, parent): poly_loop = self.gbxml.add_element(parent, "PolyLoop") for vertice in polygon.vertices: cartesian_point = self.gbxml.add_element(poly_loop, "CartesianPoint") for coord in [0, 1, 2]: coordinate = self.gbxml.add_element(cartesian_point, "Coordinate") coordinate.text = str(object.data.vertices[vertice].co[coord]) def get_volume(self, o): volume = 0 ob_mat = o.matrix_world me = o.data me.calc_loop_triangles() for tf in me.loop_triangles: tfv = tf.vertices if len(tf.vertices) == 3: tf_tris = ((me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),) else: tf_tris = (me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]), ( me.vertices[tfv[2]], me.vertices[tfv[3]], me.vertices[tfv[0]], ) for tf_iter in tf_tris: v1 = ob_mat @ tf_iter[0].co v2 = ob_mat @ tf_iter[1].co v3 = ob_mat @ tf_iter[2].co volume += v1.dot(v2.cross(v3)) / 6.0 return volume gbxml_exporter = GbxmlExporter() gbxml_exporter.export()