Type properties are now shown and direct linking of IFC (background blend generation) is now possible

This commit is contained in:
Dion Moult
2024-02-24 20:31:59 +11:00
parent d02239902f
commit 67753a0a3c
6 changed files with 290 additions and 72 deletions
@@ -53,7 +53,7 @@ classes = (
operator.zzz, operator.zzz,
operator.zxc, operator.zxc,
operator.aaa, operator.aaa,
operator.asdfasdf, operator.LoadLinkedProject,
operator.QueryLinkedElement, operator.QueryLinkedElement,
prop.LibraryElement, prop.LibraryElement,
prop.FilterCategory, prop.FilterCategory,
@@ -0,0 +1,88 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
import gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
class ProjectDecorator:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = []
selected_edges = []
selected_tris = []
try:
obj = context.scene.BIMProjectProperties.queried_obj
selected_vertices = obj["selected_vertices"]
selected_edges = obj["selected_edges"]
selected_tris = obj["selected_tris"]
except:
return
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
@@ -21,6 +21,7 @@ import bpy
import time import time
import logging import logging
import tempfile import tempfile
import subprocess
import numpy as np import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
@@ -814,16 +815,17 @@ class ToggleFilterCategories(bpy.types.Operator):
class LinkIfc(bpy.types.Operator): class LinkIfc(bpy.types.Operator):
bl_idname = "bim.link_ifc" bl_idname = "bim.link_ifc"
bl_label = "Link Blend/IFC File" bl_label = "Link IFC"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "This will link the Blender file that is synced with the IFC file" bl_description = "Reference in a read-only IFC model in the background"
filepath: bpy.props.StringProperty(subtype="FILE_PATH") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
directory: bpy.props.StringProperty(subtype="DIR_PATH") directory: bpy.props.StringProperty(subtype="DIR_PATH")
filter_glob: bpy.props.StringProperty(default="*.blend;*.blend1", options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
def execute(self, context): def execute(self, context):
start = time.time()
files = [f.name for f in self.files] if self.files else [self.filepath] files = [f.name for f in self.files] if self.files else [self.filepath]
for filename in files: for filename in files:
filepath = os.path.join(self.directory, filename) filepath = os.path.join(self.directory, filename)
@@ -835,6 +837,7 @@ class LinkIfc(bpy.types.Operator):
filepath = os.path.relpath(filepath, bpy.path.abspath("//")) filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
new.name = filepath new.name = filepath
bpy.ops.bim.load_link(filepath=filepath) bpy.ops.bim.load_link(filepath=filepath)
print(f"Finished linking {len(files)} IFCs", time.time() - start)
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@@ -890,6 +893,13 @@ class LoadLink(bpy.types.Operator):
filepath = self.filepath filepath = self.filepath
if not os.path.isabs(filepath): if not os.path.isabs(filepath):
filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath)) filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath))
if self.filepath.lower().endswith(".blend"):
self.link_blend(filepath)
elif self.filepath.lower().endswith(".ifc"):
self.link_ifc()
return {"FINISHED"}
def link_blend(self, filepath):
with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to): with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
data_to.scenes = data_from.scenes data_to.scenes = data_from.scenes
for scene in bpy.data.scenes: for scene in bpy.data.scenes:
@@ -899,9 +909,36 @@ class LoadLink(bpy.types.Operator):
if "IfcProject" not in child.name: if "IfcProject" not in child.name:
continue continue
bpy.data.scenes[0].collection.children.link(child) bpy.data.scenes[0].collection.children.link(child)
link = context.scene.BIMProjectProperties.links.get(self.filepath) link = bpy.context.scene.BIMProjectProperties.links.get(self.filepath)
link.is_loaded = True link.is_loaded = True
return {"FINISHED"}
def link_ifc(self):
blend_filepath = self.filepath + ".cache.blend"
h5_filepath = self.filepath + ".cache.h5"
if not os.path.exists(blend_filepath):
code = f"""
import bpy
def run():
bpy.ops.bim.load_linked_project(filepath="{self.filepath}")
bpy.ops.wm.save_as_mainfile(filepath="{blend_filepath}")
try:
run()
except Exception as e:
import traceback
traceback.print_exc()
exit(1)
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
temp_file.write(code)
run = subprocess.run([bpy.app.binary_path, "-b", "--python", temp_file.name, "--python-exit-code", "1"])
if run.returncode == 1:
print("An error occurred while processing your IFC.")
self.link_blend(blend_filepath)
class ReloadLink(bpy.types.Operator): class ReloadLink(bpy.types.Operator):
@@ -1143,7 +1180,6 @@ class xxx(bpy.types.Operator):
def execute(self, context): def execute(self, context):
import time import time
import multiprocessing import multiprocessing
import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
import numpy as np import numpy as np
from mathutils import Matrix from mathutils import Matrix
@@ -1540,83 +1576,99 @@ class aaa(bpy.types.Operator):
return obj return obj
class asdfasdf(bpy.types.Operator): class LoadLinkedProject(bpy.types.Operator):
bl_idname = "bim.asdfasdf" bl_idname = "bim.load_linked_project"
bl_label = "Regular IFC iterator with C++ chunking" bl_label = "Load a project for viewing only."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
import time import ifcpatch
import multiprocessing import multiprocessing
import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
import numpy as np
from mathutils import Matrix
import resource
mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000
start = time.time() start = time.time()
self.collection = bpy.data.collections.new("Project") self.filepath = self.filepath or "/home/dion/drive/ifcs/racbasicsampleproject.ifc"
# ifc_file = ifcopenshell.open('/home/dion/test.ifc')
ifc_file = ifcopenshell.open("/home/dion/drive/ifcs/racbasicsampleproject.ifc") self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath))
# ifc_file = ifcopenshell.open('/home/dion/drive/ifcs/TXG_sample_project-fixed-IFC4.ifc') self.file = ifcopenshell.open(self.filepath)
# ifc_file = ifcopenshell.open("/home/dion/tmp/petrubug/F-ELECT.ifc") # self.file = ifcopenshell.open('/home/dion/test.ifc')
# self.file = ifcopenshell.open('/home/dion/drive/ifcs/TXG_sample_project-fixed-IFC4.ifc')
# self.file = ifcopenshell.open("/home/dion/tmp/petrubug/F-ELECT.ifc")
print("Finished opening") print("Finished opening")
settings = ifcopenshell.geom.settings() self.db_filepath = self.filepath + ".cache.sqlite"
# settings.set_context_ids([ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW").id()]) db = ifcpatch.execute(
els = set(ifc_file.by_type("IfcElement")) {"input": self.filepath, "file": self.file, "recipe": "ExtractPropertiesToSQLite", "arguments": []}
els |= set(ifc_file.by_type("IfcSite")) )
els -= set(ifc_file.by_type("IfcFeatureElement")) ifcpatch.write(db, self.db_filepath)
els = list(els) print("Finished writing property database")
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=els)
meshes = {}
blender_mats = {}
total_materials = 0 logger = logging.getLogger("ImportIFC")
self.materials = [] ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = self.file
ifc_importer.process_context_filter()
ci = 0 self.elements = set(self.file.by_type("IfcElement"))
if iterator.initialize(): if self.file.schema in ("IFC2X3", "IFC4"):
while True: self.elements |= set(self.file.by_type("IfcProxy"))
has_processed_chunk = False self.elements |= set(self.file.by_type("IfcSite"))
if iterator.process_chunk(): self.elements -= set(self.file.by_type("IfcFeatureElement"))
has_processed_chunk = True self.elements = list(self.elements)
ci += 1
print("Doing chunk", ci)
e = iterator.get_chunk()
for c in e.colours: for settings in ifc_importer.context_settings:
blender_mat = bpy.data.materials.new(str(total_materials)) iterator = ifcopenshell.geom.iterator(
blender_mat.diffuse_color = list(c) settings, self.file, multiprocessing.cpu_count(), include=self.elements
self.materials.append(blender_mat) )
total_materials += 1 meshes = {}
o = self.create_object(e.get_verts(), e.get_faces(), e.get_materials(), e.get_material_ids()) blender_mats = {}
o["guids"] = list(e.guids)
o["guid_ids"] = list(e.guid_ids) total_materials = 0
if not iterator.next(): self.materials = []
if not has_processed_chunk:
# The left over chunk ci = 0
e = iterator.get_chunk() if iterator.initialize():
for c in e.colours: while True:
has_processed_chunk = False
if iterator.process_chunk():
has_processed_chunk = True
ci += 1
if ci % 50 == 0:
print("Doing chunk", ci)
chunk = iterator.get_chunk()
for colour in chunk.colours:
blender_mat = bpy.data.materials.new(str(total_materials)) blender_mat = bpy.data.materials.new(str(total_materials))
blender_mat.diffuse_color = list(c) blender_mat.diffuse_color = list(colour)
self.materials.append(blender_mat) self.materials.append(blender_mat)
total_materials += 1 total_materials += 1
self.create_object(chunk)
o = self.create_object(e.get_verts(), e.get_faces(), e.get_materials(), e.get_material_ids()) if not iterator.next():
o["guids"] = list(e.guids) if not has_processed_chunk:
o["guid_ids"] = list(e.guid_ids) # The left over chunk
break chunk = iterator.get_chunk()
for colour in chunk.colours:
blender_mat = bpy.data.materials.new(str(total_materials))
blender_mat.diffuse_color = list(colour)
self.materials.append(blender_mat)
total_materials += 1
self.create_object(chunk)
break
bpy.context.scene.collection.children.link(self.collection) bpy.context.scene.collection.children.link(self.collection)
print("Finished", time.time() - start) print("Finished", time.time() - start)
newmem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000 break
print("Mem", newmem - mem)
return {"FINISHED"} return {"FINISHED"}
def create_object(self, verts, faces, materials, material_ids): def create_object(self, chunk):
verts = chunk.get_verts()
faces = chunk.get_faces()
materials = chunk.get_materials()
material_ids = chunk.get_material_ids()
num_vertices = len(verts) // 3 num_vertices = len(verts) // 3
if not num_vertices: if not num_vertices:
return return
@@ -1645,9 +1697,12 @@ class asdfasdf(bpy.types.Operator):
mesh.update() mesh.update()
obj = bpy.data.objects.new("Blah", mesh) obj = bpy.data.objects.new("Chunk", mesh)
obj["guids"] = list(chunk.guids)
obj["guid_ids"] = list(chunk.guid_ids)
obj["db"] = self.db_filepath
self.collection.objects.link(obj) self.collection.objects.link(obj)
return obj
class QueryLinkedElement(bpy.types.Operator): class QueryLinkedElement(bpy.types.Operator):
@@ -1660,10 +1715,20 @@ class QueryLinkedElement(bpy.types.Operator):
return context.area.type == "VIEW_3D" return context.area.type == "VIEW_3D"
def execute(self, context): def execute(self, context):
import sqlite3
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
LinksData.linked_data = {}
bpy.context.scene.BIMProjectProperties.queried_obj = None bpy.context.scene.BIMProjectProperties.queried_obj = None
for area in bpy.context.screen.areas:
if area.type == "PROPERTIES":
for region in area.regions:
if region.type == "WINDOW":
region.tag_redraw()
elif area.type == "VIEW_3D":
area.tag_redraw()
region = context.region region = context.region
rv3d = context.region_data rv3d = context.region_data
coord = (self.mouse_x, self.mouse_y) coord = (self.mouse_x, self.mouse_y)
@@ -1704,9 +1769,7 @@ class QueryLinkedElement(bpy.types.Operator):
break break
guid_start_index = guid_end_index guid_start_index = guid_end_index
import sqlite3 self.db = sqlite3.connect(obj["db"])
self.db = sqlite3.connect("/home/dion/Projects/ifcopenshell/src/ifcclash/foo.sqlite")
self.c = self.db.cursor() self.c = self.db.cursor()
self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1")
@@ -1724,9 +1787,27 @@ class QueryLinkedElement(bpy.types.Operator):
for row in rows: for row in rows:
properties.setdefault(row[1], {})[row[2]] = row[3] properties.setdefault(row[1], {})[row[2]] = row[3]
sorted_properties = [(k, properties[k]) for k in sorted(properties.keys())] self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],))
relationships = self.c.fetchall()
LinksData.linked_data = {"attributes": attributes, "properties": sorted_properties} relating_type_id = None
for relationship in relationships:
if relationship[1] == "IfcRelDefinesByType":
relating_type_id = relationship[2]
type_properties = {}
if relating_type_id is not None:
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,))
rows = self.c.fetchall()
for row in rows:
type_properties.setdefault(row[1], {})[row[2]] = row[3]
LinksData.linked_data = {
"attributes": attributes,
"properties": [(k, properties[k]) for k in sorted(properties.keys())],
"type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())],
}
for area in bpy.context.screen.areas: for area in bpy.context.screen.areas:
if area.type == "PROPERTIES": if area.type == "PROPERTIES":
@@ -324,6 +324,18 @@ class BIM_PT_links(Panel):
row.label(text=name) row.label(text=name)
row.label(text=value) row.label(text=value)
if LinksData.linked_data["type_properties"]:
row = self.layout.row()
row.label(icon="LINKED", text="Type Properties")
for pset in LinksData.linked_data["type_properties"]:
box = self.layout.box()
row = box.row(align=True)
row.label(text=pset[0], icon="COPY_ID")
for name, value in pset[1].items():
row = box.row(align=True)
row.label(text=name)
row.label(text=value)
class BIM_UL_library(UIList): class BIM_UL_library(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
@@ -0,0 +1,33 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import WorkSpaceTool
class QueryTool(bpy.types.WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'OBJECT'
bl_idname = "bim.query_tool"
bl_label = "Query Tool"
bl_description = "Fetch data about a linked IFC element"
bl_icon = "ops.generic.select_circle"
bl_widget = None
bl_keymap = (
("bim.query_linked_element", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
)
@@ -117,8 +117,12 @@ class Patcher:
psets = ifcopenshell.util.element.get_psets(element, should_inherit=False) psets = ifcopenshell.util.element.get_psets(element, should_inherit=False)
for pset_name, pset_data in psets.items(): for pset_name, pset_data in psets.items():
for prop_name, value in pset_data.items(): for prop_name, value in pset_data.items():
if prop_name == "id": if prop_name == "id" or value is None or value == "":
continue continue
if isinstance(value, bool):
value = "True" if value else "False"
elif not isinstance(value, str):
value = str(value)
properties.append([i, pset_name, prop_name, value]) properties.append([i, pset_name, prop_name, value])
relating_type = ifcopenshell.util.element.get_type(element) relating_type = ifcopenshell.util.element.get_type(element)
if relating_type and relating_type != element: if relating_type and relating_type != element: