From 0b6c8fe83e104b41fbbfc6e36b09fd735ef173af Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 2 Dec 2022 19:41:34 +0600 Subject: [PATCH] Added base for ifc sverchok modifier It's located in "Modifiers" section of the properties (the same way as IFC Array or IFC Stair). It requires Sverchok addon to be enabled (it's lazy importing it when it's needed, so there is no new constant dependency). What you can now do with it: 1) Add sverchok graphs to this modifier and store simple data from sverchok in IFC ("Update data from sverchok). The "output" node of the sverchok graph has to be type of "Viewer Draw" and it's name should start with "ifcoutput" (no case-sensitive). 2) Import/export json of sverchok graphs. --- .../blenderbim/bim/module/model/__init__.py | 26 +- .../blenderbim/bim/module/model/data.py | 21 ++ .../blenderbim/bim/module/model/prop.py | 6 +- .../bim/module/model/sverchok_modifier.py | 300 ++++++++++++++++++ .../blenderbim/bim/module/model/ui.py | 38 ++- 5 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py 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"