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.zxc,
operator.aaa,
operator.asdfasdf,
operator.LoadLinkedProject,
operator.QueryLinkedElement,
prop.LibraryElement,
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 logging
import tempfile
import subprocess
import numpy as np
import ifcopenshell
import ifcopenshell.api
@@ -814,16 +815,17 @@ class ToggleFilterCategories(bpy.types.Operator):
class LinkIfc(bpy.types.Operator):
bl_idname = "bim.link_ifc"
bl_label = "Link Blend/IFC File"
bl_label = "Link IFC"
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")
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
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)
def execute(self, context):
start = time.time()
files = [f.name for f in self.files] if self.files else [self.filepath]
for filename in files:
filepath = os.path.join(self.directory, filename)
@@ -835,6 +837,7 @@ class LinkIfc(bpy.types.Operator):
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
new.name = filepath
bpy.ops.bim.load_link(filepath=filepath)
print(f"Finished linking {len(files)} IFCs", time.time() - start)
return {"FINISHED"}
def invoke(self, context, event):
@@ -890,6 +893,13 @@ class LoadLink(bpy.types.Operator):
filepath = self.filepath
if not os.path.isabs(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):
data_to.scenes = data_from.scenes
for scene in bpy.data.scenes:
@@ -899,9 +909,36 @@ class LoadLink(bpy.types.Operator):
if "IfcProject" not in child.name:
continue
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
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):
@@ -1143,7 +1180,6 @@ class xxx(bpy.types.Operator):
def execute(self, context):
import time
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
import numpy as np
from mathutils import Matrix
@@ -1540,83 +1576,99 @@ class aaa(bpy.types.Operator):
return obj
class asdfasdf(bpy.types.Operator):
bl_idname = "bim.asdfasdf"
bl_label = "Regular IFC iterator with C++ chunking"
class LoadLinkedProject(bpy.types.Operator):
bl_idname = "bim.load_linked_project"
bl_label = "Load a project for viewing only."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty()
def execute(self, context):
import time
import ifcpatch
import multiprocessing
import ifcopenshell
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()
self.collection = bpy.data.collections.new("Project")
# ifc_file = ifcopenshell.open('/home/dion/test.ifc')
ifc_file = ifcopenshell.open("/home/dion/drive/ifcs/racbasicsampleproject.ifc")
# ifc_file = ifcopenshell.open('/home/dion/drive/ifcs/TXG_sample_project-fixed-IFC4.ifc')
# ifc_file = ifcopenshell.open("/home/dion/tmp/petrubug/F-ELECT.ifc")
self.filepath = self.filepath or "/home/dion/drive/ifcs/racbasicsampleproject.ifc"
self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath))
self.file = ifcopenshell.open(self.filepath)
# 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")
settings = ifcopenshell.geom.settings()
# settings.set_context_ids([ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW").id()])
els = set(ifc_file.by_type("IfcElement"))
els |= set(ifc_file.by_type("IfcSite"))
els -= set(ifc_file.by_type("IfcFeatureElement"))
els = list(els)
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=els)
meshes = {}
blender_mats = {}
self.db_filepath = self.filepath + ".cache.sqlite"
db = ifcpatch.execute(
{"input": self.filepath, "file": self.file, "recipe": "ExtractPropertiesToSQLite", "arguments": []}
)
ifcpatch.write(db, self.db_filepath)
print("Finished writing property database")
total_materials = 0
self.materials = []
logger = logging.getLogger("ImportIFC")
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
if iterator.initialize():
while True:
has_processed_chunk = False
if iterator.process_chunk():
has_processed_chunk = True
ci += 1
print("Doing chunk", ci)
e = iterator.get_chunk()
self.elements = set(self.file.by_type("IfcElement"))
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements |= set(self.file.by_type("IfcProxy"))
self.elements |= set(self.file.by_type("IfcSite"))
self.elements -= set(self.file.by_type("IfcFeatureElement"))
self.elements = list(self.elements)
for c in e.colours:
blender_mat = bpy.data.materials.new(str(total_materials))
blender_mat.diffuse_color = list(c)
self.materials.append(blender_mat)
total_materials += 1
o = self.create_object(e.get_verts(), e.get_faces(), e.get_materials(), e.get_material_ids())
o["guids"] = list(e.guids)
o["guid_ids"] = list(e.guid_ids)
if not iterator.next():
if not has_processed_chunk:
# The left over chunk
e = iterator.get_chunk()
for c in e.colours:
for settings in ifc_importer.context_settings:
iterator = ifcopenshell.geom.iterator(
settings, self.file, multiprocessing.cpu_count(), include=self.elements
)
meshes = {}
blender_mats = {}
total_materials = 0
self.materials = []
ci = 0
if iterator.initialize():
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.diffuse_color = list(c)
blender_mat.diffuse_color = list(colour)
self.materials.append(blender_mat)
total_materials += 1
self.create_object(chunk)
o = self.create_object(e.get_verts(), e.get_faces(), e.get_materials(), e.get_material_ids())
o["guids"] = list(e.guids)
o["guid_ids"] = list(e.guid_ids)
break
if not iterator.next():
if not has_processed_chunk:
# The left over chunk
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)
print("Finished", time.time() - start)
newmem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000
print("Mem", newmem - mem)
bpy.context.scene.collection.children.link(self.collection)
print("Finished", time.time() - start)
break
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
if not num_vertices:
return
@@ -1645,9 +1697,12 @@ class asdfasdf(bpy.types.Operator):
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)
return obj
class QueryLinkedElement(bpy.types.Operator):
@@ -1660,10 +1715,20 @@ class QueryLinkedElement(bpy.types.Operator):
return context.area.type == "VIEW_3D"
def execute(self, context):
import sqlite3
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
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
rv3d = context.region_data
coord = (self.mouse_x, self.mouse_y)
@@ -1704,9 +1769,7 @@ class QueryLinkedElement(bpy.types.Operator):
break
guid_start_index = guid_end_index
import sqlite3
self.db = sqlite3.connect("/home/dion/Projects/ifcopenshell/src/ifcclash/foo.sqlite")
self.db = sqlite3.connect(obj["db"])
self.c = self.db.cursor()
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:
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:
if area.type == "PROPERTIES":
@@ -324,6 +324,18 @@ class BIM_PT_links(Panel):
row.label(text=name)
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):
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)
for pset_name, pset_data in psets.items():
for prop_name, value in pset_data.items():
if prop_name == "id":
if prop_name == "id" or value is None or value == "":
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])
relating_type = ifcopenshell.util.element.get_type(element)
if relating_type and relating_type != element: