diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 5b7d60ffc7..1e24ba138e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -17,7 +17,22 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from . import handler, prop, ui, grid, array, product, wall, slab, stair, opening, pie, workspace, profile +from . import ( + handler, + prop, + ui, + grid, + array, + product, + wall, + slab, + stair, + opening, + pie, + workspace, + profile, + sverchok_modifier, +) classes = ( array.AddArray, @@ -74,9 +89,11 @@ classes = ( prop.BIMModelProperties, prop.BIMArrayProperties, prop.BIMStairProperties, + prop.BIMSverchokProperties, ui.BIM_PT_authoring, ui.BIM_PT_array, ui.BIM_PT_stair, + ui.BIM_PT_sverchok, ui.DisplayConstrTypesUI, ui.LaunchTypeManager, ui.HelpConstrTypes, @@ -94,6 +111,11 @@ classes = ( pie.PieAddOpening, pie.VIEW3D_MT_PIE_bim, pie.VIEW3D_MT_PIE_bim_class, + sverchok_modifier.CreateNewSverchokGraph, + sverchok_modifier.UpdateDataFromSverchok, + sverchok_modifier.DeleteSverchokGraph, + sverchok_modifier.ImportSverchokGraph, + sverchok_modifier.ExportSverchokGraph, ) addon_keymaps = [] @@ -105,6 +127,7 @@ def register(): bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties) bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties) bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties) + bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties) bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) bpy.types.VIEW3D_MT_add.append(ui.add_menu) @@ -123,6 +146,7 @@ def unregister(): del bpy.types.Scene.BIMModelProperties del bpy.types.Object.BIMArrayProperties del bpy.types.Object.BIMStairProperties + del bpy.types.Object.BIMSverchokProperties bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button) diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index 47b9b5039e..824716e032 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -32,6 +32,7 @@ def refresh(): AuthoringData.is_loaded = False ArrayData.is_loaded = False StairData.is_loaded = False + SverchokData.is_loaded = False class AuthoringData: @@ -374,3 +375,23 @@ class StairData: if parameters: parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]") return parameters + + +class SverchokData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {"parameters": cls.parameters()} + + @classmethod + def parameters(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + if element: + psets = ifcopenshell.util.element.get_psets(element) + parameters = psets.get("BBIM_Sverchok", None) + if parameters: + parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]") + return parameters diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index bc40d98210..2636deccf7 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -20,7 +20,7 @@ import bpy from blenderbim.bim.prop import ObjProperty from blenderbim.bim.module.model.data import AuthoringData from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound -from bpy.types import PropertyGroup +from bpy.types import PropertyGroup, NodeTree def get_ifc_class(self, context): @@ -294,3 +294,7 @@ class BIMStairProperties(PropertyGroup): "tread_depth": self.tread_depth, "tread_run": self.tread_run, } + + +class BIMSverchokProperties(PropertyGroup): + node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree) diff --git a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py new file mode 100644 index 0000000000..619c871c58 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py @@ -0,0 +1,300 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021, 2022 Dion Moult , @Andrej730 +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on 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. +# +# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see . + +import bpy +from bpy.types import Operator +from bpy.props import FloatProperty, IntProperty, BoolProperty +from bpy_extras.object_utils import AddObjectHelper, object_data_add + +import bmesh +from bmesh.types import BMVert + +import ifcopenshell +import blenderbim +import blenderbim.tool as tool +from blenderbim.bim.module.model.prop import BIMStairProperties + + +from mathutils import Vector +from pprint import pprint +import json +import zipfile + +from os.path import basename, dirname +import os.path +import json + + +def update_sverchok_modifier(context): + obj = context.active_object + props = obj.BIMSverchokProperties + element = tool.Ifc.get_entity(obj) + psets = ifcopenshell.util.element.get_psets(element) + pset = psets.get("BBIM_Sverchok", None) + sverchok_data = json.loads(pset["Data"]) if pset else dict() + + vertices = sverchok_data.get("vertices", []) + edges = sverchok_data.get("edges", []) + faces = sverchok_data.get("polygons", []) + + bm = bmesh.new() + bm.verts.index_update() + bm.edges.index_update() + bm.faces.index_update() + + new_verts = [bm.verts.new(v) for v in vertices] + new_edges = [bm.edges.new([new_verts[vi] for vi in edge]) for edge in edges] + new_faces = [bm.faces.new([new_verts[vi] for vi in face]) for face in faces] + bm.verts.index_update() + bm.edges.index_update() + bm.faces.index_update() + + if context.object.mode == "EDIT": + bmesh.update_edit_mesh(obj.data) + else: + bm.to_mesh(obj.data) + bm.free() + obj.data.update() + + +# UI operators +class CreateNewSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.create_new_sverchok_graph" + bl_label = "Create new sverchok graph" + bl_options = {"REGISTER"} + + def _execute(self, context): + import sverchok + + obj = context.active_object + props = obj.BIMSverchokProperties + + node_group = bpy.data.node_groups.new("IfcNodeTree", type="SverchCustomTreeType") + plane = node_group.nodes.new(type="SvPlaneNodeMk3") + plane.sizex = 1 + plane.sizey = 1 + sverchok.ui.sv_temporal_viewers.add_temporal_viewer_draw( + node_group.nodes, node_group.links, plane, cut_links=True + ) + viewer = [n for n in node_group.nodes if n.bl_idname == "SvViewerDrawMk4"][0] + viewer.label = f"IFCOutput {viewer.label}" + + props.node_group = node_group + return {"FINISHED"} + + +class DeleteSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.delete_sverchok_graph" + bl_label = "Delete selected sverchok graph" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMSverchokProperties + bpy.data.node_groups.remove(props.node_group) + return {"FINISHED"} + + def draw(self, context): + self.layout.label(text="WARNING. The graph will be removed permamently.") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + +class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.update_data_from_sverchok" + bl_label = "Update data from sverchok" + bl_options = {"REGISTER"} + + def invoke(self, context, event): + if not context.active_object.BIMSverchokProperties.node_group: + return context.window_manager.invoke_props_dialog(self) + return self._execute(context) + + def draw(self, context): + self.layout.label(text="WARNING. Sverchok data will be emptied") + self.layout.label(text="because no sverchok node group is selected.") + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMSverchokProperties + node_group = props.node_group + + if node_group: + output_node = [ + n + for n in node_group.nodes + if n.bl_idname == "SvViewerDrawMk4" and n.label.lower().startswith("ifcoutput") + ] + + if not output_node: + error_msg = ( + f"Couldn't find output node in node group {node_group.name}.\n" + 'Output node should be type of "Viewer Draw" and it\'s label should start with "ifcoutput".' + ) + self.report({"ERROR"}, (error_msg)) + return {"CANCELLED"} + + output_node = output_node[0] + + sverchok_data = { + "vertices": output_node.inputs["Vertices"].sv_get(deepcopy=False, default=[]), + "edges": output_node.inputs["Edges"].sv_get(deepcopy=False, default=[]), + "polygons": output_node.inputs["Polygons"].sv_get(deepcopy=False, default=[]), + } + # sv_get returns array of arrays of 1 element, for convenience we reduce it to just 1 level array + def try_first_element(x): + return x[0] if x else x + + for key in sverchok_data: + sverchok_data[key] = try_first_element(sverchok_data[key]) + else: + sverchok_data = {"vertices": [], "edges": [], "polygons": []} + + print("sverchok data to upload: ", sverchok_data) + psets = ifcopenshell.util.element.get_psets(element) + pset = psets.get("BBIM_Sverchok", None) + + if pset: + pset = tool.Ifc.get().by_id(pset["id"]) + else: + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Sverchok") + + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Data": json.dumps(sverchok_data)}, + ) + update_sverchok_modifier(context) + + return {"FINISHED"} + + +# Basically copied Sverchok SvNodeTreeImporter +# Wasn't able to use actual SvNodeTreeImporter because it expects node graph to be opened +class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.import_sverchok_graph" + bl_label = "Import sverchok graph" + bl_options = {"REGISTER"} + + filepath: bpy.props.StringProperty( + name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" + ) + + filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + + def _execute(self, context): + import sverchok + + importer = sverchok.utils.sv_json_import.JSONImporter.init_from_path(self.filepath) + obj = context.active_object + props = obj.BIMSverchokProperties + + node_group = context.scene.io_panel_properties.import_tree + if not node_group: + node_group = props.node_group or bpy.data.node_groups.new("IfcNodeTree", type="SverchCustomTreeType") + + importer.import_into_tree(node_group) + if importer.has_fails: + self.report({"ERROR"}, importer.fail_massage) + props.node_group = node_group + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def draw(self, context): + col = self.layout.column() + col.label(text="Destination tree to import JSON:") + col.template_ID(context.scene.io_panel_properties, "import_tree", new="node.new_import_tree") + + +# Basically copied Sverchok SvNodeTreeExporter +# Wasn't able to use actual SvNodeTreeExporter because it expects node graph to be opened +class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.export_sverchok_graph" + bl_label = "Export sverchok graph" + bl_options = {"REGISTER"} + + filepath: bpy.props.StringProperty( + name="File Path", description="Filepath used for exporting to", maxlen=1024, default="", subtype="FILE_PATH" + ) + + filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + + compact: bpy.props.BoolProperty(default=True, description="Compact representation of the JSON file") + compress: bpy.props.BoolProperty() + + def _execute(self, context): + import sverchok + + obj = context.active_object + props = obj.BIMSverchokProperties + ng = props.node_group + destination_path = self.filepath + if not destination_path.lower().endswith(".json"): + destination_path += ".json" + + # future: should check if filepath is a folder or ends in \ + layout_dict = sverchok.utils.sv_json_export.JSONExporter.get_tree_structure(ng) + + if not layout_dict: + msg = "no update list found - didn't export" + self.report({"WARNING"}, msg) + return {"CANCELLED"} + + indent = None if self.compact else 2 + with open(destination_path, "w") as fpo: + json.dump(layout_dict, fpo, indent=indent) # json_struct doesn't expect sort_keys = True + msg = "exported to: " + destination_path + self.report({"INFO"}, msg) + + if self.compress: + comp_mode = zipfile.ZIP_DEFLATED + + # destination path = /a../b../c../somename.json + base = basename(destination_path) # somename.json + basedir = dirname(destination_path) # /a../b../c../ + + # somename.zip + final_archivename = base.replace(".json", "") + ".zip" + + # /a../b../c../somename.zip + fullpath = os.path.join(basedir, final_archivename) + + with zipfile.ZipFile(fullpath, "w", compression=comp_mode) as myzip: + myzip.write(destination_path, arcname=base) + + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def draw(self, context): + graph_name = context.active_object.BIMSverchokProperties.node_group.name + self.layout.label(text=f'Save node tree "{graph_name}" into json:') + + col = self.layout.column(heading="Options") # new syntax in >= 2.90 + col.use_property_split = True + col.prop(self, "compact") + col.prop(self, "compress", text="Create ZIP archive") diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 0341a869f9..846c6ac364 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -19,9 +19,10 @@ import bpy import blenderbim.tool as tool from bpy.types import Panel, Operator, Menu -from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData +from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData from blenderbim.bim.module.model.prop import store_cursor_position from blenderbim.bim.module.model.stair import update_stair_modifier +from blenderbim.bim.module.model.sverchok_modifier import update_sverchok_modifier from blenderbim.bim.helper import prop_with_search @@ -368,6 +369,41 @@ class BIM_PT_stair(bpy.types.Panel): row.operator("bim.add_stair", icon="ADD", text="") +class BIM_PT_sverchok(bpy.types.Panel): + bl_label = "IFC Sverchok" + bl_idname = "BIM_PT_sverchok" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "modifier" + + @classmethod + def poll(cls, context): + # always display modifier if it's IFC object + return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object) + + def draw(self, context): + if not SverchokData.is_loaded: + SverchokData.load() + + self.layout.label(text="Requires SVERCHOK addon to be enabled.", icon="SEQUENCE_COLOR_02") + + props = context.active_object.BIMSverchokProperties + self.layout.prop_search(props, "node_group", bpy.data, "node_groups") + self.layout.operator("bim.create_new_sverchok_graph", icon="ADD") + + self.layout.operator("bim.update_data_from_sverchok", icon="FILE_REFRESH") + + row = self.layout.row() + row.operator("bim.delete_sverchok_graph", icon="X") + row.enabled = bool(props.node_group) + + self.layout.operator("bim.import_sverchok_graph", text="Import JSON", icon="RNA") + + row = self.layout.row() + row.operator("bim.export_sverchok_graph", text="Export to JSON", icon="FILE_BACKUP") + row.enabled = bool(props.node_group) + + class BIM_MT_model(Menu): bl_idname = "BIM_MT_model" bl_label = "IFC Objects"