mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-19 03:33:48 +00:00
5655df2081
Example - https://i.imgur.com/0op89zh.png
2275 lines
92 KiB
Python
2275 lines
92 KiB
Python
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
|
# Copyright (C) 2020, 2021 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 os
|
|
import bpy
|
|
import time
|
|
import json
|
|
import logging
|
|
import tempfile
|
|
import traceback
|
|
import subprocess
|
|
import datetime
|
|
import numpy as np
|
|
import ifcopenshell
|
|
import ifcopenshell.api
|
|
import ifcopenshell.util.file
|
|
import ifcopenshell.util.selector
|
|
import ifcopenshell.util.geolocation
|
|
import ifcopenshell.util.representation
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.shape
|
|
import blenderbim.bim.handler
|
|
import blenderbim.bim.schema
|
|
import blenderbim.tool as tool
|
|
import blenderbim.core.project as core
|
|
import blenderbim.core.context
|
|
import blenderbim.core.owner
|
|
import blenderbim.core.spatial
|
|
from blenderbim.bim.ifc import IfcStore
|
|
from blenderbim.bim.ui import IFCFileSelector
|
|
from blenderbim.bim import import_ifc
|
|
from blenderbim.bim import export_ifc
|
|
from collections import defaultdict
|
|
from math import radians
|
|
from pathlib import Path
|
|
from mathutils import Vector, Matrix
|
|
from bpy.app.handlers import persistent
|
|
from ifcopenshell.geom import ShapeElementType
|
|
from blenderbim.bim.module.project.data import LinksData
|
|
from blenderbim.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
|
|
from typing import Union
|
|
|
|
|
|
class NewProject(bpy.types.Operator):
|
|
bl_idname = "bim.new_project"
|
|
bl_label = "New Project"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Start a new IFC project in a fresh session"
|
|
preset: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
bpy.ops.wm.read_homefile()
|
|
|
|
if self.preset == "metric_m":
|
|
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
|
|
bpy.context.scene.unit_settings.system = "METRIC"
|
|
bpy.context.scene.unit_settings.length_unit = "METERS"
|
|
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
|
|
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
|
|
bpy.context.scene.BIMProjectProperties.template_file = "0"
|
|
elif self.preset == "metric_mm":
|
|
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
|
|
bpy.context.scene.unit_settings.system = "METRIC"
|
|
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
|
|
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
|
|
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
|
|
bpy.context.scene.BIMProjectProperties.template_file = "0"
|
|
elif self.preset == "imperial_ft":
|
|
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
|
|
bpy.context.scene.unit_settings.system = "IMPERIAL"
|
|
bpy.context.scene.unit_settings.length_unit = "FEET"
|
|
bpy.context.scene.BIMProperties.area_unit = "square foot"
|
|
bpy.context.scene.BIMProperties.volume_unit = "cubic foot"
|
|
bpy.context.scene.BIMProjectProperties.template_file = "0"
|
|
elif self.preset == "demo":
|
|
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
|
|
bpy.context.scene.unit_settings.system = "METRIC"
|
|
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
|
|
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
|
|
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
|
|
bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc"
|
|
|
|
if self.preset != "wizard":
|
|
bpy.ops.bim.create_project()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class CreateProject(bpy.types.Operator):
|
|
bl_idname = "bim.create_project"
|
|
bl_label = "Create Project"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Create a new IFC project"
|
|
|
|
def execute(self, context):
|
|
IfcStore.begin_transaction(self)
|
|
IfcStore.add_transaction_operation(self, rollback=self.rollback, commit=lambda data: True)
|
|
self._execute(context)
|
|
self.transaction_data = {"file": tool.Ifc.get()}
|
|
IfcStore.add_transaction_operation(self, rollback=lambda data: True, commit=self.commit)
|
|
IfcStore.end_transaction(self)
|
|
return {"FINISHED"}
|
|
|
|
def _execute(self, context):
|
|
props = context.scene.BIMProjectProperties
|
|
template = None if props.template_file == "0" else props.template_file
|
|
if tool.Blender.is_default_scene():
|
|
for obj in bpy.data.objects:
|
|
bpy.data.objects.remove(obj)
|
|
for mesh in bpy.data.meshes:
|
|
bpy.data.meshes.remove(mesh)
|
|
for mat in bpy.data.materials:
|
|
bpy.data.materials.remove(mat)
|
|
core.create_project(tool.Ifc, tool.Project, tool.Spatial, schema=props.export_schema, template=template)
|
|
blenderbim.bim.schema.reload(tool.Ifc.get().schema_identifier)
|
|
tool.Blender.register_toolbar()
|
|
|
|
def rollback(self, data):
|
|
IfcStore.file = None
|
|
|
|
def commit(self, data):
|
|
IfcStore.file = data["file"]
|
|
|
|
|
|
class SelectLibraryFile(bpy.types.Operator, IFCFileSelector):
|
|
bl_idname = "bim.select_library_file"
|
|
bl_label = "Select Library File"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Select an IFC file that can be used as a library"
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
|
append_all: bpy.props.BoolProperty(default=False)
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
|
|
|
def execute(self, context):
|
|
IfcStore.begin_transaction(self)
|
|
old_filepath = IfcStore.library_path
|
|
result = self._execute(context)
|
|
self.transaction_data = {"old_filepath": old_filepath, "filepath": self.get_filepath()}
|
|
IfcStore.add_transaction_operation(self)
|
|
IfcStore.end_transaction(self)
|
|
return result
|
|
|
|
def _execute(self, context):
|
|
filepath = self.get_filepath()
|
|
IfcStore.library_path = filepath
|
|
IfcStore.library_file = ifcopenshell.open(filepath)
|
|
bpy.ops.bim.refresh_library()
|
|
if context.area:
|
|
context.area.tag_redraw()
|
|
if self.append_all:
|
|
bpy.ops.bim.append_entire_library()
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def rollback(self, data):
|
|
if data["old_filepath"]:
|
|
IfcStore.library_path = data["old_filepath"]
|
|
IfcStore.library_file = ifcopenshell.open(data["old_filepath"])
|
|
else:
|
|
IfcStore.library_path = ""
|
|
IfcStore.library_file = None
|
|
|
|
def commit(self, data):
|
|
IfcStore.library_path = data["filepath"]
|
|
IfcStore.library_file = ifcopenshell.open(data["filepath"])
|
|
|
|
def draw(self, context):
|
|
self.layout.prop(self, "append_all", text="Append Entire Library")
|
|
|
|
|
|
class RefreshLibrary(bpy.types.Operator):
|
|
bl_idname = "bim.refresh_library"
|
|
bl_label = "Refresh Library"
|
|
|
|
def execute(self, context):
|
|
self.props = context.scene.BIMProjectProperties
|
|
|
|
self.props.library_elements.clear()
|
|
self.props.library_breadcrumb.clear()
|
|
|
|
self.props.active_library_element = ""
|
|
|
|
types = IfcStore.library_file.wrapped_data.types_with_super()
|
|
|
|
element_classes = ["IfcTypeProduct", "IfcMaterial", "IfcCostSchedule", "IfcProfileDef"]
|
|
for importable_type in sorted(element_classes):
|
|
if importable_type in types:
|
|
new = self.props.library_elements.add()
|
|
new.name = importable_type
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ChangeLibraryElement(bpy.types.Operator):
|
|
bl_idname = "bim.change_library_element"
|
|
bl_label = "Change Library Element"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
element_name: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
self.props = context.scene.BIMProjectProperties
|
|
self.file = IfcStore.get_file()
|
|
self.library_file = IfcStore.library_file
|
|
ifc_classes = set()
|
|
self.props.active_library_element = self.element_name
|
|
|
|
crumb = self.props.library_breadcrumb.add()
|
|
crumb.name = self.element_name
|
|
|
|
elements = self.library_file.by_type(self.element_name)
|
|
[ifc_classes.add(e.is_a()) for e in elements]
|
|
|
|
self.props.library_elements.clear()
|
|
|
|
if len(ifc_classes) == 1 and list(ifc_classes)[0] == self.element_name:
|
|
for name, ifc_definition_id in sorted([(self.get_name(e), e.id()) for e in elements]):
|
|
self.add_library_asset(name, ifc_definition_id)
|
|
else:
|
|
for ifc_class in sorted(ifc_classes):
|
|
if ifc_class == self.element_name:
|
|
continue
|
|
new = self.props.library_elements.add()
|
|
new.name = ifc_class
|
|
for name, ifc_definition_id, ifc_class in sorted([(self.get_name(e), e.id(), e.is_a()) for e in elements]):
|
|
if ifc_class == self.element_name:
|
|
self.add_library_asset(name, ifc_definition_id)
|
|
return {"FINISHED"}
|
|
|
|
def get_name(self, element):
|
|
if element.is_a("IfcProfileDef"):
|
|
return element.ProfileName or "Unnamed"
|
|
return element.Name or "Unnamed"
|
|
|
|
def add_library_asset(self, name, ifc_definition_id):
|
|
new = self.props.library_elements.add()
|
|
new.name = name
|
|
new.ifc_definition_id = ifc_definition_id
|
|
element = self.library_file.by_id(ifc_definition_id)
|
|
if self.library_file.schema == "IFC2X3" or not self.library_file.by_type("IfcProjectLibrary"):
|
|
new.is_declared = False
|
|
elif getattr(element, "HasContext", None) and element.HasContext[0].RelatingContext.is_a("IfcProjectLibrary"):
|
|
new.is_declared = True
|
|
try:
|
|
if element.is_a("IfcMaterial"):
|
|
next(e for e in self.file.by_type("IfcMaterial") if e.Name == name)
|
|
elif element.is_a("IfcProfileDef"):
|
|
next(e for e in self.file.by_type("IfcProfileDef") if e.ProfileName == name)
|
|
else:
|
|
self.file.by_guid(element.GlobalId)
|
|
new.is_appended = True
|
|
except (AttributeError, RuntimeError, StopIteration):
|
|
new.is_appended = False
|
|
|
|
|
|
class RewindLibrary(bpy.types.Operator):
|
|
bl_idname = "bim.rewind_library"
|
|
bl_label = "Rewind Library"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
self.props = context.scene.BIMProjectProperties
|
|
total_breadcrumbs = len(self.props.library_breadcrumb)
|
|
if total_breadcrumbs < 2:
|
|
bpy.ops.bim.refresh_library()
|
|
return {"FINISHED"}
|
|
element_name = self.props.library_breadcrumb[total_breadcrumbs - 2].name
|
|
self.props.library_breadcrumb.remove(total_breadcrumbs - 1)
|
|
self.props.library_breadcrumb.remove(total_breadcrumbs - 2)
|
|
bpy.ops.bim.change_library_element(element_name=element_name)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AssignLibraryDeclaration(bpy.types.Operator):
|
|
bl_idname = "bim.assign_library_declaration"
|
|
bl_label = "Assign Library Declaration"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
definition: bpy.props.IntProperty()
|
|
|
|
def execute(self, context):
|
|
IfcStore.begin_transaction(self)
|
|
IfcStore.library_file.begin_transaction()
|
|
result = self._execute(context)
|
|
IfcStore.library_file.end_transaction()
|
|
IfcStore.add_transaction_operation(self)
|
|
IfcStore.end_transaction(self)
|
|
return result
|
|
|
|
def _execute(self, context):
|
|
self.props = context.scene.BIMProjectProperties
|
|
self.file = IfcStore.library_file
|
|
ifcopenshell.api.run(
|
|
"project.assign_declaration",
|
|
self.file,
|
|
definitions=[self.file.by_id(self.definition)],
|
|
relating_context=self.file.by_type("IfcProjectLibrary")[0],
|
|
)
|
|
element_name = self.props.active_library_element
|
|
bpy.ops.bim.rewind_library()
|
|
bpy.ops.bim.change_library_element(element_name=element_name)
|
|
return {"FINISHED"}
|
|
|
|
def rollback(self, data):
|
|
IfcStore.library_file.undo()
|
|
|
|
def commit(self, data):
|
|
IfcStore.library_file.redo()
|
|
|
|
|
|
class UnassignLibraryDeclaration(bpy.types.Operator):
|
|
bl_idname = "bim.unassign_library_declaration"
|
|
bl_label = "Unassign Library Declaration"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
definition: bpy.props.IntProperty()
|
|
|
|
def execute(self, context):
|
|
IfcStore.begin_transaction(self)
|
|
IfcStore.library_file.begin_transaction()
|
|
result = self._execute(context)
|
|
IfcStore.library_file.end_transaction()
|
|
IfcStore.add_transaction_operation(self)
|
|
IfcStore.end_transaction(self)
|
|
return result
|
|
|
|
def _execute(self, context):
|
|
self.props = context.scene.BIMProjectProperties
|
|
self.file = IfcStore.library_file
|
|
ifcopenshell.api.run(
|
|
"project.unassign_declaration",
|
|
self.file,
|
|
definitions=[self.file.by_id(self.definition)],
|
|
relating_context=self.file.by_type("IfcProjectLibrary")[0],
|
|
)
|
|
element_name = self.props.active_library_element
|
|
bpy.ops.bim.rewind_library()
|
|
bpy.ops.bim.change_library_element(element_name=element_name)
|
|
return {"FINISHED"}
|
|
|
|
def rollback(self, data):
|
|
IfcStore.library_file.undo()
|
|
|
|
def commit(self, data):
|
|
IfcStore.library_file.redo()
|
|
|
|
|
|
class SaveLibraryFile(bpy.types.Operator):
|
|
bl_idname = "bim.save_library_file"
|
|
bl_label = "Save Library File"
|
|
|
|
def execute(self, context):
|
|
IfcStore.library_file.write(IfcStore.library_path)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AppendEntireLibrary(bpy.types.Operator):
|
|
bl_idname = "bim.append_entire_library"
|
|
bl_label = "Append Entire Library"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return IfcStore.get_file()
|
|
|
|
def execute(self, context):
|
|
return IfcStore.execute_ifc_operator(self, context)
|
|
|
|
def _execute(self, context):
|
|
self.file = IfcStore.get_file()
|
|
self.library = IfcStore.library_file
|
|
|
|
lib_elements = ifcopenshell.util.selector.filter_elements(
|
|
self.library, "IfcTypeProduct, IfcMaterial, IfcCostSchedule, IfcProfileDef"
|
|
)
|
|
for element in lib_elements:
|
|
bpy.ops.bim.append_library_element(definition=element.id())
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AppendLibraryElementByQuery(bpy.types.Operator):
|
|
bl_idname = "bim.append_library_element_by_query"
|
|
bl_label = "Append Library Element By Query"
|
|
query: bpy.props.StringProperty(name="Query")
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return IfcStore.get_file()
|
|
|
|
def execute(self, context):
|
|
return IfcStore.execute_ifc_operator(self, context)
|
|
|
|
def _execute(self, context):
|
|
self.file = IfcStore.get_file()
|
|
self.library = IfcStore.library_file
|
|
|
|
for element in ifcopenshell.util.selector.filter_elements(self.library, self.query):
|
|
bpy.ops.bim.append_library_element(definition=element.id())
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
self.layout.prop(self, "query")
|
|
|
|
|
|
class AppendLibraryElement(bpy.types.Operator):
|
|
bl_idname = "bim.append_library_element"
|
|
bl_label = "Append Library Element"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Append element to the current project"
|
|
definition: bpy.props.IntProperty()
|
|
prop_index: bpy.props.IntProperty()
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
poll = bool(IfcStore.get_file())
|
|
if bpy.app.version > (3, 0, 0) and not poll:
|
|
cls.poll_message_set("Please create or load a project first.")
|
|
return poll
|
|
|
|
def execute(self, context):
|
|
return IfcStore.execute_ifc_operator(self, context)
|
|
|
|
def _execute(self, context):
|
|
self.file = IfcStore.get_file()
|
|
element = ifcopenshell.api.run(
|
|
"project.append_asset",
|
|
self.file,
|
|
library=IfcStore.library_file,
|
|
element=IfcStore.library_file.by_id(self.definition),
|
|
)
|
|
if not element:
|
|
return {"FINISHED"}
|
|
if element.is_a("IfcTypeProduct"):
|
|
self.import_type_from_ifc(element, context)
|
|
elif element.is_a("IfcProduct"):
|
|
# NOTE: not used as UI doesn't allow appending non-types
|
|
self.import_product_from_ifc(element, context)
|
|
element_type = ifcopenshell.util.element.get_type(element)
|
|
obj = tool.Ifc.get_object(element_type)
|
|
if obj is None:
|
|
self.import_type_from_ifc(element_type, context)
|
|
|
|
elif element.is_a("IfcMaterial"):
|
|
self.import_material_from_ifc(element, context)
|
|
try:
|
|
context.scene.BIMProjectProperties.library_elements[self.prop_index].is_appended = True
|
|
except:
|
|
# TODO Remove this terrible code when I refactor this into the core
|
|
pass
|
|
blenderbim.bim.handler.refresh_ui_data()
|
|
return {"FINISHED"}
|
|
|
|
def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
|
self.file = IfcStore.get_file()
|
|
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
|
|
self.import_material_styles(element, ifc_importer)
|
|
|
|
def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
|
self.file = IfcStore.get_file()
|
|
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()
|
|
ifc_importer.material_creator.load_existing_materials()
|
|
self.import_materials(element, ifc_importer)
|
|
self.import_styles(element, ifc_importer)
|
|
ifc_importer.create_generic_elements({element})
|
|
ifc_importer.place_objects_in_collections()
|
|
|
|
def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
|
self.file = IfcStore.get_file()
|
|
logger = logging.getLogger("ImportIFC")
|
|
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
|
|
|
|
type_collection = bpy.data.collections.get("Types")
|
|
if not type_collection:
|
|
type_collection = bpy.data.collections.new("Types")
|
|
for collection in bpy.context.view_layer.layer_collection.children:
|
|
collection_obj = collection.collection.BIMCollectionProperties.obj
|
|
if collection_obj and tool.Ifc.get_entity(collection_obj).is_a("IfcProject"):
|
|
collection.collection.children.link(type_collection)
|
|
collection.children["Types"].hide_viewport = True
|
|
break
|
|
|
|
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
|
ifc_importer.file = self.file
|
|
ifc_importer.type_collection = type_collection
|
|
ifc_importer.process_context_filter()
|
|
ifc_importer.material_creator.load_existing_materials()
|
|
self.import_materials(element, ifc_importer)
|
|
self.import_styles(element, ifc_importer)
|
|
ifc_importer.create_element_type(element)
|
|
ifc_importer.place_objects_in_collections()
|
|
|
|
def import_materials(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None:
|
|
for material in ifcopenshell.util.element.get_materials(element):
|
|
if IfcStore.get_element(material.id()):
|
|
continue
|
|
self.import_material_styles(material, ifc_importer)
|
|
|
|
def import_styles(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None:
|
|
if element.is_a("IfcTypeProduct"):
|
|
representations = element.RepresentationMaps or []
|
|
elif element.is_a("IfcProduct"):
|
|
representations = [element.Representation] if element.Representation else []
|
|
for representation in representations or []:
|
|
for element in self.file.traverse(representation):
|
|
if not element.is_a("IfcRepresentationItem") or not element.StyledByItem:
|
|
continue
|
|
for element2 in self.file.traverse(element.StyledByItem[0]):
|
|
if element2.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element2.id()):
|
|
ifc_importer.create_style(element2)
|
|
|
|
def import_material_styles(
|
|
self,
|
|
material: ifcopenshell.entity_instance,
|
|
ifc_importer: import_ifc.IfcImporter,
|
|
) -> None:
|
|
if not material.HasRepresentation:
|
|
return
|
|
for element in self.file.traverse(material.HasRepresentation[0]):
|
|
if element.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element.id()):
|
|
ifc_importer.create_style(element)
|
|
|
|
|
|
class EnableEditingHeader(bpy.types.Operator):
|
|
bl_idname = "bim.enable_editing_header"
|
|
bl_label = "Enable Editing Header"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Edit the IFC header file such as Author, Organization, ..."
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return IfcStore.get_file()
|
|
|
|
def execute(self, context):
|
|
self.file = IfcStore.get_file()
|
|
props = context.scene.BIMProjectProperties
|
|
props.is_editing = True
|
|
|
|
mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description)
|
|
if "[" in mvd:
|
|
props.mvd = mvd.split("[")[1][0:-1]
|
|
else:
|
|
props.mvd = ""
|
|
|
|
author = self.file.wrapped_data.header.file_name.author
|
|
if author:
|
|
props.author_name = author[0]
|
|
if len(author) > 1:
|
|
props.author_email = author[1]
|
|
|
|
organisation = self.file.wrapped_data.header.file_name.organization
|
|
if organisation:
|
|
props.organisation_name = organisation[0]
|
|
if len(organisation) > 1:
|
|
props.organisation_email = organisation[1]
|
|
|
|
props.authorisation = self.file.wrapped_data.header.file_name.authorization
|
|
return {"FINISHED"}
|
|
|
|
|
|
class EditHeader(bpy.types.Operator):
|
|
bl_idname = "bim.edit_header"
|
|
bl_label = "Edit Header"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Save header information"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return IfcStore.get_file()
|
|
|
|
def execute(self, context):
|
|
IfcStore.begin_transaction(self)
|
|
self.transaction_data = {}
|
|
self.transaction_data["old"] = self.record_state()
|
|
result = self._execute(context)
|
|
self.transaction_data["new"] = self.record_state()
|
|
IfcStore.add_transaction_operation(self)
|
|
IfcStore.end_transaction(self)
|
|
return result
|
|
|
|
def _execute(self, context):
|
|
self.file = IfcStore.get_file()
|
|
props = context.scene.BIMProjectProperties
|
|
props.is_editing = True
|
|
|
|
self.file.wrapped_data.header.file_description.description = (f"ViewDefinition[{props.mvd}]",)
|
|
self.file.wrapped_data.header.file_name.author = (props.author_name, props.author_email)
|
|
self.file.wrapped_data.header.file_name.organization = (props.organisation_name, props.organisation_email)
|
|
self.file.wrapped_data.header.file_name.authorization = props.authorisation
|
|
bpy.ops.bim.disable_editing_header()
|
|
return {"FINISHED"}
|
|
|
|
def record_state(self):
|
|
self.file = IfcStore.get_file()
|
|
return {
|
|
"description": self.file.wrapped_data.header.file_description.description,
|
|
"author": self.file.wrapped_data.header.file_name.author,
|
|
"organisation": self.file.wrapped_data.header.file_name.organization,
|
|
"authorisation": self.file.wrapped_data.header.file_name.authorization,
|
|
}
|
|
|
|
def rollback(self, data):
|
|
file = IfcStore.get_file()
|
|
file.wrapped_data.header.file_description.description = data["old"]["description"]
|
|
file.wrapped_data.header.file_name.author = data["old"]["author"]
|
|
file.wrapped_data.header.file_name.organization = data["old"]["organisation"]
|
|
file.wrapped_data.header.file_name.authorization = data["old"]["authorisation"]
|
|
|
|
def commit(self, data):
|
|
file = IfcStore.get_file()
|
|
file.wrapped_data.header.file_description.description = data["new"]["description"]
|
|
file.wrapped_data.header.file_name.author = data["new"]["author"]
|
|
file.wrapped_data.header.file_name.organization = data["new"]["organisation"]
|
|
file.wrapped_data.header.file_name.authorization = data["new"]["authorisation"]
|
|
|
|
|
|
class DisableEditingHeader(bpy.types.Operator):
|
|
bl_idname = "bim.disable_editing_header"
|
|
bl_label = "Disable Editing Header"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Cancel unsaved header information"
|
|
|
|
def execute(self, context):
|
|
context.scene.BIMProjectProperties.is_editing = False
|
|
return {"FINISHED"}
|
|
|
|
|
|
class LoadProject(bpy.types.Operator, IFCFileSelector):
|
|
bl_idname = "bim.load_project"
|
|
bl_label = "Load Project"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Load an existing IFC project"
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"})
|
|
is_advanced: bpy.props.BoolProperty(name="Enable Advanced Mode", default=False)
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
|
should_start_fresh_session: bpy.props.BoolProperty(name="Should Start Fresh Session", default=True)
|
|
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
tooltip = cls.bl_description
|
|
if not properties.use_detailed_tooltip:
|
|
return tooltip
|
|
|
|
filepath = properties.filepath
|
|
if not filepath:
|
|
return tooltip
|
|
filepath = Path(filepath)
|
|
tooltip += f".\n"
|
|
if not filepath.exists():
|
|
tooltip += "\nFile does not exist"
|
|
return tooltip
|
|
|
|
def get_modified_date(st_mtime: float) -> str:
|
|
mod_time = datetime.datetime.fromtimestamp(st_mtime)
|
|
|
|
today = datetime.date.today()
|
|
if mod_time.date() == today:
|
|
return f"Today {mod_time.strftime('%I:%M %p')}"
|
|
elif mod_time.date() == today - datetime.timedelta(days=1):
|
|
return f"Yesterday {mod_time.strftime('%I:%M %p')}"
|
|
return mod_time.strftime("%d %b %Y")
|
|
|
|
def get_file_size(size_bytes: float) -> str:
|
|
if size_bytes < 1024 * 1024: # Less than 1 MiB
|
|
size = size_bytes / 1024
|
|
return f"{size:.0f} KiB"
|
|
else:
|
|
size = size_bytes / (1024 * 1024)
|
|
return f"{size:.1f} MiB"
|
|
|
|
extractor = ifcopenshell.util.file.IfcHeaderExtractor(str(filepath))
|
|
header_metadata = extractor.extract()
|
|
if schema := header_metadata.get("schema_name"):
|
|
tooltip += f"\nSchema: {schema}"
|
|
|
|
file_stat = filepath.stat()
|
|
tooltip += f"\nModified: {get_modified_date(file_stat.st_mtime)}"
|
|
tooltip += f"\nSize: {get_file_size(file_stat.st_size)}"
|
|
|
|
return tooltip
|
|
|
|
def execute(self, context):
|
|
@persistent
|
|
def load_handler(*args):
|
|
bpy.app.handlers.load_post.remove(load_handler)
|
|
self.finish_loading_project(context)
|
|
|
|
if self.should_start_fresh_session:
|
|
# WARNING: wm.read_homefile clears context which could lead to some
|
|
# operators to fail:
|
|
# https://blender.stackexchange.com/a/282558/135166
|
|
# So we continue using the load_post handler thats triggered when
|
|
# context is already restored
|
|
bpy.app.handlers.load_post.append(load_handler)
|
|
bpy.ops.wm.read_homefile()
|
|
return {"FINISHED"}
|
|
else:
|
|
return self.finish_loading_project(context)
|
|
|
|
def finish_loading_project(self, context):
|
|
try:
|
|
if not self.is_existing_ifc_file():
|
|
return {"FINISHED"}
|
|
|
|
if tool.Blender.is_default_scene():
|
|
for obj in bpy.data.objects:
|
|
bpy.data.objects.remove(obj)
|
|
|
|
filepath = Path(self.get_filepath())
|
|
context.scene.BIMProperties.ifc_file = str(filepath)
|
|
context.scene.BIMProjectProperties.is_loading = True
|
|
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
|
tool.Blender.register_toolbar()
|
|
tool.Blender.add_recent_ifc_project(filepath.absolute())
|
|
|
|
if not self.is_advanced:
|
|
bpy.ops.bim.load_project_elements()
|
|
except:
|
|
blenderbim.last_error = traceback.format_exc()
|
|
raise
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def draw(self, context):
|
|
self.layout.prop(self, "is_advanced")
|
|
IFCFileSelector.draw(self, context)
|
|
|
|
|
|
class ClearRecentIFCProjects(bpy.types.Operator):
|
|
bl_idname = "bim.clear_recent_ifc_projects"
|
|
bl_label = "Clear Recent IFC Projects List"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
tool.Blender.clear_recent_ifc_projects()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class RevertProject(bpy.types.Operator, IFCFileSelector):
|
|
bl_idname = "bim.revert_project"
|
|
bl_label = "Revert IFC Project"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Revert to a fresh session discarding all unsaved changes"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not context.scene.BIMProperties.ifc_file:
|
|
cls.poll_message_set("IFC project need to be loaded and saved on the disk.")
|
|
return False
|
|
return True
|
|
|
|
def execute(self, context):
|
|
bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=context.scene.BIMProperties.ifc_file)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class LoadProjectElements(bpy.types.Operator):
|
|
bl_idname = "bim.load_project_elements"
|
|
bl_label = "Load Project Elements"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
self.props = context.scene.BIMProjectProperties
|
|
self.file = IfcStore.get_file()
|
|
blenderbim.bim.schema.reload(self.file.schema_identifier)
|
|
start = time.time()
|
|
logger = logging.getLogger("ImportIFC")
|
|
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
|
|
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
|
|
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
|
|
logging.basicConfig(
|
|
filename=path_log,
|
|
filemode="a",
|
|
level=logging.DEBUG,
|
|
)
|
|
settings = import_ifc.IfcImportSettings.factory(context, context.scene.BIMProperties.ifc_file, logger)
|
|
settings.has_filter = self.props.filter_mode != "NONE"
|
|
settings.should_filter_spatial_elements = self.props.should_filter_spatial_elements
|
|
if self.props.filter_mode == "DECOMPOSITION":
|
|
settings.elements = self.get_decomposition_elements()
|
|
elif self.props.filter_mode == "IFC_CLASS":
|
|
settings.elements = self.get_ifc_class_elements()
|
|
elif self.props.filter_mode == "IFC_TYPE":
|
|
settings.elements = self.get_ifc_type_elements()
|
|
elif self.props.filter_mode == "WHITELIST":
|
|
settings.elements = self.get_whitelist_elements()
|
|
elif self.props.filter_mode == "BLACKLIST":
|
|
settings.elements = self.get_blacklist_elements()
|
|
settings.logger.info("Starting import")
|
|
ifc_importer = import_ifc.IfcImporter(settings)
|
|
ifc_importer.execute()
|
|
settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start))
|
|
print("Import finished in {:.2f} seconds".format(time.time() - start))
|
|
context.scene.BIMProjectProperties.is_loading = False
|
|
|
|
tool.Project.load_pset_templates()
|
|
tool.Project.load_default_thumbnails()
|
|
tool.Project.set_default_context()
|
|
tool.Project.set_default_modeling_dimensions()
|
|
return {"FINISHED"}
|
|
|
|
def get_decomposition_elements(self):
|
|
containers = set()
|
|
for filter_category in self.props.filter_categories:
|
|
if not filter_category.is_selected:
|
|
continue
|
|
container = self.file.by_id(filter_category.ifc_definition_id)
|
|
while container:
|
|
containers.add(container)
|
|
container = ifcopenshell.util.element.get_aggregate(container)
|
|
if self.file.schema == "IFC2X3" and container.is_a("IfcProject"):
|
|
container = None
|
|
elif self.file.schema != "IFC2X3" and container.is_a("IfcContext"):
|
|
container = None
|
|
elements = set()
|
|
for container in containers:
|
|
for rel in container.ContainsElements:
|
|
elements.update(rel.RelatedElements)
|
|
self.append_decomposed_elements(elements)
|
|
return elements
|
|
|
|
def append_decomposed_elements(self, elements):
|
|
decomposed_elements = set()
|
|
for element in elements:
|
|
if element.IsDecomposedBy:
|
|
for subelement in element.IsDecomposedBy[0].RelatedObjects:
|
|
decomposed_elements.add(subelement)
|
|
if decomposed_elements:
|
|
self.append_decomposed_elements(decomposed_elements)
|
|
elements.update(decomposed_elements)
|
|
|
|
def get_ifc_class_elements(self):
|
|
elements = set()
|
|
for filter_category in self.props.filter_categories:
|
|
if not filter_category.is_selected:
|
|
continue
|
|
elements.update(self.file.by_type(filter_category.name, include_subtypes=False))
|
|
return elements
|
|
|
|
def get_ifc_type_elements(self):
|
|
elements = set()
|
|
for filter_category in self.props.filter_categories:
|
|
if not filter_category.is_selected:
|
|
continue
|
|
elements.update(ifcopenshell.util.element.get_types(self.file.by_id(filter_category.ifc_definition_id)))
|
|
return elements
|
|
|
|
def get_whitelist_elements(self):
|
|
return set(ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query))
|
|
|
|
def get_blacklist_elements(self):
|
|
return set(self.file.by_type("IfcElement")) - set(
|
|
ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query)
|
|
)
|
|
|
|
|
|
class ToggleFilterCategories(bpy.types.Operator):
|
|
bl_idname = "bim.toggle_filter_categories"
|
|
bl_label = "Toggle Filter Categories"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
|
|
|
|
def execute(self, context):
|
|
for filter_category in context.scene.BIMProjectProperties.filter_categories:
|
|
filter_category.is_selected = self.should_select
|
|
return {"FINISHED"}
|
|
|
|
|
|
class LinkIfc(bpy.types.Operator):
|
|
bl_idname = "bim.link_ifc"
|
|
bl_label = "Link IFC"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
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="*.ifc", options={"HIDDEN"})
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
|
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
|
|
|
def draw(self, context):
|
|
pprops = context.scene.BIMProjectProperties
|
|
row = self.layout.row()
|
|
row.prop(self, "use_relative_path")
|
|
row = self.layout.row()
|
|
row.prop(self, "use_cache")
|
|
row = self.layout.row()
|
|
row.prop(pprops, "false_origin_mode")
|
|
if pprops.false_origin_mode == "MANUAL":
|
|
row = self.layout.row()
|
|
row.prop(pprops, "false_origin")
|
|
|
|
def execute(self, context):
|
|
start = time.time()
|
|
files = [f.name.replace("\\", "/") for f in self.files] if self.files else [self.filepath.replace("\\", "/")]
|
|
for filename in files:
|
|
filepath = os.path.join(self.directory, filename).replace("\\", "/")
|
|
if bpy.data.filepath and Path(filepath).samefile(bpy.data.filepath):
|
|
self.report({"INFO"}, "Can't link the current .blend file")
|
|
continue
|
|
new = context.scene.BIMProjectProperties.links.add()
|
|
if self.use_relative_path:
|
|
try:
|
|
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
|
|
except:
|
|
pass # Perhaps on another drive or something
|
|
new.name = filepath
|
|
status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache)
|
|
if status == {"CANCELLED"}:
|
|
error_msg = (
|
|
f'Error processing IFC file "{self.filepath}" '
|
|
"was critical and blend file either wasn't saved or wasn't updated. "
|
|
"See logs above in system console for details."
|
|
)
|
|
print(error_msg)
|
|
self.report({"ERROR"}, error_msg)
|
|
return {"FINISHED"}
|
|
print(f"Finished linking {len(files)} IFCs", time.time() - start)
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class UnlinkIfc(bpy.types.Operator):
|
|
bl_idname = "bim.unlink_ifc"
|
|
bl_label = "UnLink IFC"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Remove the selected file from the link list"
|
|
filepath: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
self.filepath = self.filepath.replace("\\", "/")
|
|
bpy.ops.bim.unload_link(filepath=self.filepath)
|
|
index = context.scene.BIMProjectProperties.links.find(self.filepath)
|
|
if index != -1:
|
|
context.scene.BIMProjectProperties.links.remove(index)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class UnloadLink(bpy.types.Operator):
|
|
bl_idname = "bim.unload_link"
|
|
bl_label = "Unload Link"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Unload the selected linked file"
|
|
filepath: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
self.filepath = self.filepath.replace("\\", "/")
|
|
filepath = self.filepath
|
|
if not os.path.isabs(filepath):
|
|
filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath))
|
|
filepath = Path(filepath)
|
|
if filepath.suffix.lower() == ".ifc":
|
|
filepath = filepath.with_suffix(".ifc.cache.blend")
|
|
|
|
for collection in context.scene.collection.children[:]:
|
|
if collection.library and Path(collection.library.filepath) == filepath:
|
|
bpy.data.collections.remove(collection)
|
|
|
|
for scene in bpy.data.scenes[:]:
|
|
if scene.library and Path(scene.library.filepath) == filepath:
|
|
bpy.data.scenes.remove(scene)
|
|
|
|
links = context.scene.BIMProjectProperties.links
|
|
links.get(self.filepath).is_loaded = False
|
|
|
|
if not any([l.is_loaded for l in links]):
|
|
ProjectDecorator.uninstall()
|
|
# we make sure we don't draw queried object from the file that was just unlinked
|
|
elif queried_obj := context.scene.BIMProjectProperties.queried_obj:
|
|
queried_filepath = Path(queried_obj["ifc_filepath"])
|
|
if queried_filepath == filepath:
|
|
ProjectDecorator.uninstall()
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class LoadLink(bpy.types.Operator):
|
|
bl_idname = "bim.load_link"
|
|
bl_label = "Load Link"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Load the selected file"
|
|
filepath: bpy.props.StringProperty()
|
|
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
|
|
|
def execute(self, context):
|
|
self.filepath = self.filepath.replace("\\", "/")
|
|
filepath = self.filepath
|
|
if not os.path.isabs(filepath):
|
|
filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath)).replace("\\", "/")
|
|
if self.filepath.lower().endswith(".blend"):
|
|
self.link_blend(filepath)
|
|
elif self.filepath.lower().endswith(".ifc"):
|
|
status = self.link_ifc()
|
|
if status:
|
|
return status
|
|
return {"FINISHED"}
|
|
|
|
def link_blend(self, filepath: str) -> None:
|
|
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:
|
|
if not scene.library or scene.library.filepath.replace("\\", "/") != filepath:
|
|
continue
|
|
for child in scene.collection.children:
|
|
if "IfcProject" not in child.name:
|
|
continue
|
|
bpy.data.scenes[0].collection.children.link(child)
|
|
link = bpy.context.scene.BIMProjectProperties.links.get(self.filepath)
|
|
link.is_loaded = True
|
|
|
|
def link_ifc(self) -> Union[set[str], None]:
|
|
blend_filepath = self.filepath + ".cache.blend"
|
|
h5_filepath = self.filepath + ".cache.h5"
|
|
|
|
if not self.use_cache and os.path.exists(blend_filepath):
|
|
os.remove(blend_filepath)
|
|
|
|
if not os.path.exists(blend_filepath):
|
|
pprops = bpy.context.scene.BIMProjectProperties
|
|
gprops = bpy.context.scene.BIMGeoreferenceProperties
|
|
tool.Loader.calculate_model_origin(tool.Ifc.get())
|
|
code = f"""
|
|
import bpy
|
|
|
|
def run():
|
|
gprops = bpy.context.scene.BIMGeoreferenceProperties
|
|
# Our model origin becomes their host model origin
|
|
gprops.host_model_origin = "{gprops.model_origin}"
|
|
gprops.has_blender_offset = {gprops.has_blender_offset}
|
|
gprops.blender_eastings = "{gprops.blender_eastings}"
|
|
gprops.blender_northings = "{gprops.blender_northings}"
|
|
gprops.blender_orthogonal_height = "{gprops.blender_orthogonal_height}"
|
|
gprops.blender_offset_x = "{gprops.blender_offset_x}"
|
|
gprops.blender_offset_y = "{gprops.blender_offset_y}"
|
|
gprops.blender_offset_z = "{gprops.blender_offset_z}"
|
|
gprops.blender_x_axis_abscissa = "{gprops.blender_x_axis_abscissa}"
|
|
gprops.blender_x_axis_ordinate = "{gprops.blender_x_axis_ordinate}"
|
|
pprops = bpy.context.scene.BIMProjectProperties
|
|
pprops.distance_limit = {pprops.distance_limit}
|
|
pprops.false_origin_mode = "{pprops.false_origin_mode}"
|
|
pprops.false_origin = "{pprops.false_origin}"
|
|
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)
|
|
"""
|
|
|
|
t = time.time()
|
|
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.")
|
|
if not os.path.exists(blend_filepath) or os.stat(blend_filepath).st_mtime < t:
|
|
return {"CANCELLED"}
|
|
|
|
self.set_model_origin_from_link()
|
|
|
|
self.link_blend(blend_filepath)
|
|
|
|
def set_model_origin_from_link(self):
|
|
if tool.Ifc.get():
|
|
return # The current model's coordinates always take priority.
|
|
|
|
json_filepath = self.filepath + ".cache.json"
|
|
if not os.path.exists(json_filepath):
|
|
return
|
|
|
|
with open(json_filepath, "r") as f:
|
|
data = json.load(f)
|
|
|
|
gprops = bpy.context.scene.BIMGeoreferenceProperties
|
|
for prop in ("model_origin", "blender_x_axis_abscissa", "blender_x_axis_ordinate"):
|
|
if (value := data.get(prop, None)) is not None:
|
|
setattr(gprops, prop, value)
|
|
|
|
|
|
class ReloadLink(bpy.types.Operator):
|
|
bl_idname = "bim.reload_link"
|
|
bl_label = "Reload Link"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Reload the selected file"
|
|
filepath: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
def get_linked_ifcs():
|
|
selected_filename = os.path.basename(self.filepath.replace("\\", "/"))
|
|
return [
|
|
c.library
|
|
for c in bpy.data.collections
|
|
if "IfcProject" in c.name and c.library and os.path.basename(c.library.filepath) == selected_filename
|
|
]
|
|
|
|
for library in get_linked_ifcs() or []:
|
|
library.reload()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ToggleLinkSelectability(bpy.types.Operator):
|
|
bl_idname = "bim.toggle_link_selectability"
|
|
bl_label = "Toggle Link Selectability"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Toggle selectability"
|
|
link: bpy.props.StringProperty(name="Linked IFC Filepath")
|
|
|
|
def execute(self, context):
|
|
props = context.scene.BIMProjectProperties
|
|
link = props.links.get(self.link)
|
|
if not os.path.isabs(self.link):
|
|
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
|
|
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
|
|
for collection in self.get_linked_collections():
|
|
collection.hide_select = not collection.hide_select
|
|
link.is_selectable = not collection.hide_select
|
|
return {"FINISHED"}
|
|
|
|
def get_linked_collections(self) -> list[bpy.types.Collection]:
|
|
return [
|
|
c
|
|
for c in bpy.data.collections
|
|
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
|
|
]
|
|
|
|
|
|
class ToggleLinkVisibility(bpy.types.Operator):
|
|
bl_idname = "bim.toggle_link_visibility"
|
|
bl_label = "Toggle Link Visibility"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
|
link: bpy.props.StringProperty(name="Linked IFC Filepath")
|
|
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
|
|
|
|
def execute(self, context):
|
|
props = context.scene.BIMProjectProperties
|
|
link = props.links.get(self.link)
|
|
if not os.path.isabs(self.link):
|
|
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
|
|
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
|
|
if self.mode == "WIREFRAME":
|
|
self.toggle_wireframe(link)
|
|
elif self.mode == "VISIBLE":
|
|
self.toggle_visibility(link)
|
|
return {"FINISHED"}
|
|
|
|
def toggle_wireframe(self, link):
|
|
for collection in self.get_linked_collections():
|
|
objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, collection.all_objects)
|
|
for i, obj in enumerate(objs):
|
|
if i == 0:
|
|
if obj.display_type == "WIRE":
|
|
display_type = "TEXTURED"
|
|
else:
|
|
display_type = "WIRE"
|
|
obj.display_type = display_type
|
|
link.is_wireframe = display_type == "WIRE"
|
|
|
|
def toggle_visibility(self, link):
|
|
linked_collections = self.get_linked_collections()
|
|
|
|
layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections)
|
|
for layer_collection in layer_collections.values():
|
|
layer_collection.exclude = not layer_collection.exclude
|
|
link.is_hidden = layer_collection.exclude
|
|
|
|
def get_linked_collections(self) -> list[bpy.types.Collection]:
|
|
return [
|
|
c
|
|
for c in bpy.data.collections
|
|
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
|
|
]
|
|
|
|
|
|
class ExportIFCBase:
|
|
bl_idname = "bim.save_project"
|
|
bl_label = "Save IFC"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
filename_ext = ".ifc"
|
|
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
|
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version")
|
|
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
|
|
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
|
save_as_invoked: bpy.props.BoolProperty(name="Save As Dialog Was Invoked", default=False, options={"HIDDEN"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return tool.Ifc.get()
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.prop(self, "json_version")
|
|
layout.prop(self, "json_compact")
|
|
if bpy.data.is_saved:
|
|
layout.prop(self, "use_relative_path")
|
|
|
|
def invoke(self, context, event):
|
|
if not tool.Ifc.get():
|
|
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
|
|
return {"FINISHED"}
|
|
|
|
self.save_as_invoked = False
|
|
if context.scene.BIMProperties.ifc_file and not self.should_save_as:
|
|
self.filepath = context.scene.BIMProperties.ifc_file
|
|
if not os.path.isabs(self.filepath):
|
|
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath))
|
|
return self.execute(context)
|
|
if not self.filepath:
|
|
if bpy.data.is_saved:
|
|
self.filepath = Path(bpy.data.filepath).with_suffix(".ifc").__str__()
|
|
else:
|
|
self.filepath = "untitled.ifc"
|
|
|
|
self.save_as_invoked = True
|
|
WindowManager = context.window_manager
|
|
WindowManager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def execute(self, context):
|
|
project_props = context.scene.BIMProjectProperties
|
|
if self.save_as_invoked:
|
|
project_props.use_relative_project_path = self.use_relative_path
|
|
if project_props.should_disable_undo_on_save:
|
|
old_history_size = tool.Ifc.get().history_size
|
|
old_undo_steps = context.preferences.edit.undo_steps
|
|
tool.Ifc.get().history_size = 0
|
|
context.preferences.edit.undo_steps = 0
|
|
IfcStore.execute_ifc_operator(self, context)
|
|
if project_props.should_disable_undo_on_save:
|
|
tool.Ifc.get().history_size = old_history_size
|
|
context.preferences.edit.undo_steps = old_undo_steps
|
|
return {"FINISHED"}
|
|
|
|
def _execute(self, context):
|
|
start = time.time()
|
|
logger = logging.getLogger("ExportIFC")
|
|
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
|
|
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
|
|
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
|
|
logging.basicConfig(
|
|
filename=path_log,
|
|
filemode="a",
|
|
level=logging.DEBUG,
|
|
)
|
|
extension = self.filepath.split(".")[-1]
|
|
if extension == "ifczip":
|
|
output_file = bpy.path.ensure_ext(self.filepath, ".ifczip")
|
|
elif extension == "ifcjson":
|
|
output_file = bpy.path.ensure_ext(self.filepath, ".ifcjson")
|
|
else:
|
|
output_file = bpy.path.ensure_ext(self.filepath, ".ifc")
|
|
|
|
settings = export_ifc.IfcExportSettings.factory(context, output_file, logger)
|
|
settings.json_version = self.json_version
|
|
settings.json_compact = self.json_compact
|
|
|
|
ifc_exporter = export_ifc.IfcExporter(settings)
|
|
print("Starting export")
|
|
settings.logger.info("Starting export")
|
|
ifc_exporter.export()
|
|
settings.logger.info("Export finished in {:.2f} seconds".format(time.time() - start))
|
|
print("Export finished in {:.2f} seconds".format(time.time() - start))
|
|
scene = context.scene
|
|
if not scene.DocProperties.ifc_files:
|
|
new = scene.DocProperties.ifc_files.add()
|
|
new.name = output_file
|
|
if context.scene.BIMProjectProperties.use_relative_project_path and bpy.data.is_saved:
|
|
output_file = os.path.relpath(output_file, bpy.path.abspath("//"))
|
|
if scene.BIMProperties.ifc_file != output_file and extension not in ["ifczip", "ifcjson"]:
|
|
scene.BIMProperties.ifc_file = output_file
|
|
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
|
|
if save_blend_file:
|
|
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
|
|
bpy.context.scene.BIMProperties.is_dirty = False
|
|
blenderbim.bim.handler.refresh_ui_data()
|
|
self.report(
|
|
{"INFO"},
|
|
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
|
|
)
|
|
|
|
if bpy.data.is_saved:
|
|
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
if properties.should_save_as:
|
|
return "Save the IFC file under a new name, or relocate file"
|
|
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
|
|
|
|
|
|
class ExportIFC(ExportIFCBase, bpy.types.Operator):
|
|
pass
|
|
|
|
|
|
# TODO: remove as deprecated, better wait couple releases since
|
|
# this operator is used for saving IFC files in user scripts.
|
|
class ExportIFCDeprecated(ExportIFCBase, bpy.types.Operator):
|
|
bl_idname = "export_ifc.bim"
|
|
|
|
def execute(self, context):
|
|
msg = f"'{ExportIFCDeprecated.bl_idname}' operator name is deprecated, use '{ExportIFC.bl_idname}'."
|
|
self.report({"WARNING"}, msg)
|
|
print(msg)
|
|
return super().execute(context)
|
|
|
|
|
|
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 ifcpatch
|
|
import multiprocessing
|
|
import ifcopenshell.geom
|
|
|
|
start = time.time()
|
|
|
|
pprops = bpy.context.scene.BIMProjectProperties
|
|
gprops = bpy.context.scene.BIMGeoreferenceProperties
|
|
|
|
self.filepath = self.filepath.replace("\\", "/")
|
|
print("Processing", self.filepath)
|
|
|
|
self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath))
|
|
self.file = ifcopenshell.open(self.filepath)
|
|
print("Finished opening")
|
|
|
|
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")
|
|
|
|
logger = logging.getLogger("ImportIFC")
|
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
|
tool.Loader.set_unit_scale(self.unit_scale)
|
|
tool.Loader.set_settings(import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger))
|
|
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
|
|
tool.Loader.settings.context_settings = tool.Loader.create_settings()
|
|
|
|
self.elements = set(self.file.by_type("IfcElement"))
|
|
if self.file.schema in ("IFC2X3", "IFC4"):
|
|
self.elements |= set(self.file.by_type("IfcProxy"))
|
|
if self.file.schema == "IFC2X3":
|
|
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
|
|
else:
|
|
self.elements |= set(self.file.by_type("IfcSpatialElement"))
|
|
self.elements -= set(self.file.by_type("IfcFeatureElement"))
|
|
|
|
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
|
|
tool.Loader.set_manual_blender_offset(self.file)
|
|
elif tool.Loader.settings.false_origin_mode == "AUTOMATIC":
|
|
if gprops.host_model_origin:
|
|
tool.Loader.settings.false_origin = list(map(float, gprops.host_model_origin.split(",")))
|
|
tool.Loader.set_manual_blender_offset(self.file)
|
|
else:
|
|
tool.Loader.guess_false_origin(self.file)
|
|
|
|
tool.Loader.calculate_model_origin(self.file)
|
|
self.json_filepath = self.filepath + ".cache.json"
|
|
data = {
|
|
"host_model_origin": gprops.host_model_origin,
|
|
"model_origin": gprops.model_origin,
|
|
"has_blender_offset": gprops.has_blender_offset,
|
|
"blender_eastings": gprops.blender_eastings,
|
|
"blender_northings": gprops.blender_northings,
|
|
"blender_orthogonal_height": gprops.blender_orthogonal_height,
|
|
"blender_offset_x": gprops.blender_offset_x,
|
|
"blender_offset_y": gprops.blender_offset_y,
|
|
"blender_offset_z": gprops.blender_offset_z,
|
|
"blender_x_axis_abscissa": gprops.blender_x_axis_abscissa,
|
|
"blender_x_axis_ordinate": gprops.blender_x_axis_ordinate,
|
|
"distance_limit": pprops.distance_limit,
|
|
"false_origin_mode": pprops.false_origin_mode,
|
|
"false_origin": pprops.false_origin,
|
|
}
|
|
with open(self.json_filepath, "w") as f:
|
|
json.dump(data, f)
|
|
|
|
for settings in tool.Loader.settings.context_settings:
|
|
if not self.elements:
|
|
break
|
|
|
|
results = set()
|
|
iterator = ifcopenshell.geom.iterator(
|
|
settings, self.file, multiprocessing.cpu_count(), include=self.elements
|
|
)
|
|
self.meshes = {}
|
|
self.blender_mats = {}
|
|
blender_mats = {}
|
|
|
|
default_mat = np.array([[1, 1, 1, 1]], dtype=np.float32)
|
|
chunked_guids = []
|
|
chunked_guid_ids = []
|
|
chunked_verts = []
|
|
chunked_faces = []
|
|
chunked_materials = []
|
|
chunked_material_ids = []
|
|
material_offset = 0
|
|
chunk_size = 10000
|
|
offset = 0
|
|
|
|
ci = 0
|
|
if iterator.initialize():
|
|
while True:
|
|
shape = iterator.get()
|
|
results.add(self.file.by_id(shape.id))
|
|
|
|
# Elements with a lot of geometry benefit from instancing to save memory
|
|
if len(shape.geometry.faces) > 1000: # 333 tris
|
|
self.process_occurrence(shape)
|
|
if not iterator.next():
|
|
if not chunked_verts:
|
|
break
|
|
mats = np.concatenate(chunked_materials)
|
|
midx = np.concatenate(chunked_material_ids)
|
|
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
|
midx = mapping[midx]
|
|
|
|
mat_results = []
|
|
for mat in mats:
|
|
mat = tuple(mat)
|
|
blender_mat = blender_mats.get(mat, None)
|
|
if not blender_mat:
|
|
blender_mat = bpy.data.materials.new("Chunk")
|
|
blender_mat.diffuse_color = mat
|
|
blender_mats[mat] = blender_mat
|
|
mat_results.append(blender_mat)
|
|
|
|
# The left over chunk
|
|
self.create_object(
|
|
np.concatenate(chunked_verts),
|
|
np.concatenate(chunked_faces),
|
|
mat_results,
|
|
midx,
|
|
chunked_guids,
|
|
chunked_guid_ids,
|
|
)
|
|
break
|
|
continue
|
|
|
|
ci += 1
|
|
if ci % 50 == 0:
|
|
print("Doing chunk", ci)
|
|
|
|
has_processed_chunk = False
|
|
|
|
ms = np.vstack([default_mat, np.frombuffer(shape.geometry.colors_buffer).reshape((-1, 4))])
|
|
mi = np.frombuffer(shape.geometry.material_ids_buffer, dtype=np.int32)
|
|
for geom_material_idx, geom_material in enumerate(shape.geometry.materials):
|
|
if not geom_material.instance_id():
|
|
ms[geom_material_idx + 1] = (0.8, 0.8, 0.8, 1)
|
|
chunked_materials.append(ms)
|
|
chunked_material_ids.append(mi + material_offset + 1)
|
|
material_offset += len(ms)
|
|
|
|
matrix = np.frombuffer(shape.transformation_buffer).reshape((4, 4), order="F")
|
|
if gprops.has_blender_offset:
|
|
matrix = ifcopenshell.util.geolocation.global2local(
|
|
matrix,
|
|
float(gprops.blender_offset_x) * self.unit_scale,
|
|
float(gprops.blender_offset_y) * self.unit_scale,
|
|
float(gprops.blender_offset_z) * self.unit_scale,
|
|
float(gprops.blender_x_axis_abscissa),
|
|
float(gprops.blender_x_axis_ordinate),
|
|
)
|
|
vs = np.frombuffer(shape.geometry.verts_buffer).reshape((-1, 3))
|
|
vs = np.hstack((vs, np.ones((len(vs), 1))))
|
|
vs = (np.asmatrix(matrix) * np.asmatrix(vs).T).T.A
|
|
vs = vs[:, :3].flatten()
|
|
fs = np.frombuffer(shape.geometry.faces_buffer, dtype=np.int32)
|
|
chunked_verts.append(vs)
|
|
chunked_faces.append(fs + offset)
|
|
offset += len(vs) // 3
|
|
|
|
chunked_guids.append(shape.guid)
|
|
if chunked_guid_ids:
|
|
chunked_guid_ids.append((len(fs) // 3) + chunked_guid_ids[-1])
|
|
else:
|
|
chunked_guid_ids.append(len(fs) // 3)
|
|
|
|
if offset > chunk_size:
|
|
has_processed_chunk = True
|
|
|
|
mats = np.concatenate(chunked_materials)
|
|
midx = np.concatenate(chunked_material_ids)
|
|
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
|
midx = mapping[midx]
|
|
|
|
mat_results = []
|
|
for mat in mats:
|
|
mat = tuple(mat)
|
|
blender_mat = blender_mats.get(mat, None)
|
|
if not blender_mat:
|
|
blender_mat = bpy.data.materials.new("Chunk")
|
|
blender_mat.diffuse_color = mat
|
|
blender_mats[mat] = blender_mat
|
|
mat_results.append(blender_mat)
|
|
|
|
self.create_object(
|
|
np.concatenate(chunked_verts),
|
|
np.concatenate(chunked_faces),
|
|
mat_results,
|
|
midx,
|
|
chunked_guids,
|
|
chunked_guid_ids,
|
|
)
|
|
chunked_guids = []
|
|
chunked_guid_ids = []
|
|
chunked_verts = []
|
|
chunked_faces = []
|
|
chunked_materials = []
|
|
chunked_material_ids = []
|
|
material_offset = 0
|
|
offset = 0
|
|
|
|
if not iterator.next():
|
|
if not has_processed_chunk:
|
|
mats = np.concatenate(chunked_materials)
|
|
midx = np.concatenate(chunked_material_ids)
|
|
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
|
midx = mapping[midx]
|
|
|
|
mat_results = []
|
|
for mat in mats:
|
|
mat = tuple(mat)
|
|
blender_mat = blender_mats.get(mat, None)
|
|
if not blender_mat:
|
|
blender_mat = bpy.data.materials.new("Chunk")
|
|
blender_mat.diffuse_color = mat
|
|
blender_mats[mat] = blender_mat
|
|
mat_results.append(blender_mat)
|
|
|
|
# The left over chunk
|
|
self.create_object(
|
|
np.concatenate(chunked_verts),
|
|
np.concatenate(chunked_faces),
|
|
mat_results,
|
|
midx,
|
|
chunked_guids,
|
|
chunked_guid_ids,
|
|
)
|
|
break
|
|
self.elements -= results
|
|
|
|
bpy.context.scene.collection.children.link(self.collection)
|
|
print("Finished", time.time() - start)
|
|
return {"FINISHED"}
|
|
|
|
def process_occurrence(self, shape: ShapeElementType) -> None:
|
|
element = self.file.by_id(shape.id)
|
|
faces = shape.geometry.faces
|
|
verts = shape.geometry.verts
|
|
materials = shape.geometry.materials
|
|
material_ids = shape.geometry.material_ids
|
|
|
|
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
|
|
|
|
mesh = self.meshes.get(shape.geometry.id, None)
|
|
if not mesh:
|
|
mesh = bpy.data.meshes.new("Mesh")
|
|
|
|
geometry = shape.geometry
|
|
gprops = bpy.context.scene.BIMGeoreferenceProperties
|
|
if (
|
|
gprops.has_blender_offset
|
|
and geometry.verts
|
|
and tool.Loader.is_point_far_away(
|
|
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
|
|
)
|
|
):
|
|
# Shift geometry close to the origin based off that first vert it found
|
|
verts_array = np.array(geometry.verts)
|
|
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
|
|
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
|
|
verts = offset_verts.tolist()
|
|
|
|
mesh["has_cartesian_point_offset"] = True
|
|
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}"
|
|
else:
|
|
verts = geometry.verts
|
|
mesh["has_cartesian_point_offset"] = False
|
|
|
|
material_to_slot = {}
|
|
max_slot_index = 0
|
|
|
|
for i, material in enumerate(materials):
|
|
alpha = 1.0
|
|
if material.has_transparency and material.transparency > 0:
|
|
alpha = 1.0 - material.transparency
|
|
if material.instance_id():
|
|
diffuse = material.diffuse + (alpha,)
|
|
else:
|
|
diffuse = (0.8, 0.8, 0.8, 1) # Blender's default material
|
|
material_name = f"{diffuse[0]}-{diffuse[1]}-{diffuse[2]}-{diffuse[3]}"
|
|
blender_mat = self.blender_mats.get(material_name, None)
|
|
if not blender_mat:
|
|
blender_mat = bpy.data.materials.new(material_name)
|
|
blender_mat.diffuse_color = diffuse
|
|
self.blender_mats[material_name] = blender_mat
|
|
slot_index = mesh.materials.find(material.name)
|
|
if slot_index == -1:
|
|
mesh.materials.append(blender_mat)
|
|
slot_index = max_slot_index
|
|
max_slot_index += 1
|
|
material_to_slot[i] = slot_index
|
|
|
|
material_index = [(material_to_slot[i] if i != -1 else 0) for i in material_ids]
|
|
|
|
num_vertices = len(verts) // 3
|
|
total_faces = len(faces)
|
|
loop_start = range(0, total_faces, 3)
|
|
num_loops = total_faces // 3
|
|
loop_total = [3] * num_loops
|
|
num_vertex_indices = len(faces)
|
|
|
|
mesh.vertices.add(num_vertices)
|
|
mesh.vertices.foreach_set("co", verts)
|
|
mesh.loops.add(num_vertex_indices)
|
|
mesh.loops.foreach_set("vertex_index", faces)
|
|
mesh.polygons.add(num_loops)
|
|
mesh.polygons.foreach_set("loop_start", loop_start)
|
|
mesh.polygons.foreach_set("loop_total", loop_total)
|
|
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
|
|
mesh.polygons.foreach_set("material_index", material_index)
|
|
mesh.update()
|
|
|
|
self.meshes[shape.geometry.id] = mesh
|
|
|
|
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
|
|
obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, mat)
|
|
|
|
obj["guids"] = [shape.guid]
|
|
obj["guid_ids"] = [len(mesh.polygons)]
|
|
obj["db"] = self.db_filepath
|
|
|
|
self.collection.objects.link(obj)
|
|
|
|
def create_object(self, verts, faces, materials, material_ids, guids, guid_ids):
|
|
num_vertices = len(verts) // 3
|
|
if not num_vertices:
|
|
return
|
|
total_faces = len(faces)
|
|
loop_start = range(0, total_faces, 3)
|
|
num_loops = total_faces // 3
|
|
loop_total = [3] * num_loops
|
|
num_vertex_indices = len(faces)
|
|
|
|
mesh = bpy.data.meshes.new("Mesh")
|
|
|
|
for material in materials:
|
|
mesh.materials.append(material)
|
|
|
|
mesh.vertices.add(num_vertices)
|
|
mesh.vertices.foreach_set("co", verts)
|
|
mesh.loops.add(num_vertex_indices)
|
|
mesh.loops.foreach_set("vertex_index", faces)
|
|
mesh.polygons.add(num_loops)
|
|
mesh.polygons.foreach_set("loop_start", loop_start)
|
|
mesh.polygons.foreach_set("loop_total", loop_total)
|
|
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
|
|
|
|
if material_ids.size > 0:
|
|
mesh.polygons.foreach_set("material_index", material_ids)
|
|
|
|
mesh.update()
|
|
|
|
obj = bpy.data.objects.new("Chunk", mesh)
|
|
obj["guids"] = list(guids)
|
|
obj["guid_ids"] = list(guid_ids)
|
|
obj["db"] = self.db_filepath
|
|
obj["ifc_filepath"] = self.filepath
|
|
|
|
self.collection.objects.link(obj)
|
|
|
|
|
|
class QueryLinkedElement(bpy.types.Operator):
|
|
bl_idname = "bim.query_linked_element"
|
|
bl_label = "Query Linked Element"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
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)
|
|
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
|
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
|
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
|
|
if not hit:
|
|
self.report({"INFO"}, "No object found.")
|
|
return {"FINISHED"}
|
|
|
|
if "guids" not in obj:
|
|
self.report({"INFO"}, "Object is not a linked IFC element.")
|
|
return {"FINISHED"}
|
|
|
|
guid = None
|
|
guid_start_index = 0
|
|
for i, guid_end_index in enumerate(obj["guid_ids"]):
|
|
if face_index < guid_end_index:
|
|
guid = obj["guids"][i]
|
|
bpy.context.scene.BIMProjectProperties.queried_obj = obj
|
|
|
|
selected_tris = []
|
|
selected_edges = []
|
|
vert_indices = set()
|
|
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
|
|
vert_indices.update(polygon.vertices)
|
|
vert_indices = list(vert_indices)
|
|
vert_map = {k: v for v, k in enumerate(vert_indices)}
|
|
selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices]
|
|
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
|
|
selected_tris.append(tuple(vert_map[v] for v in polygon.vertices))
|
|
selected_edges.extend(tuple([vert_map[vi] for vi in e] for e in polygon.edge_keys))
|
|
|
|
obj["selected_vertices"] = selected_vertices
|
|
obj["selected_edges"] = selected_edges
|
|
obj["selected_tris"] = selected_tris
|
|
|
|
break
|
|
guid_start_index = guid_end_index
|
|
|
|
self.db = sqlite3.connect(obj["db"])
|
|
self.c = self.db.cursor()
|
|
|
|
self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1")
|
|
element = self.c.fetchone()
|
|
|
|
attributes = {}
|
|
for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]):
|
|
if element[i + 1] is not None:
|
|
attributes[attr] = element[i + 1]
|
|
|
|
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],))
|
|
rows = self.c.fetchall()
|
|
|
|
properties = {}
|
|
for row in rows:
|
|
properties.setdefault(row[1], {})[row[2]] = row[3]
|
|
|
|
self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],))
|
|
relationships = self.c.fetchall()
|
|
|
|
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":
|
|
for region in area.regions:
|
|
if region.type == "WINDOW":
|
|
region.tag_redraw()
|
|
elif area.type == "VIEW_3D":
|
|
area.tag_redraw()
|
|
|
|
self.report({"INFO"}, f"Loaded data for {guid}")
|
|
ProjectDecorator.install(bpy.context)
|
|
return {"FINISHED"}
|
|
|
|
def ray_cast(self, context, origin, direction):
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
result = context.scene.ray_cast(depsgraph, origin, direction)
|
|
return result
|
|
|
|
def invoke(self, context, event):
|
|
self.mouse_x = event.mouse_region_x
|
|
self.mouse_y = event.mouse_region_y
|
|
return self.execute(context)
|
|
|
|
|
|
class AppendInspectedLinkedElement(AppendLibraryElement):
|
|
bl_idname = "bim.append_inspected_linked_element"
|
|
bl_label = "Append Inspected Linked Element"
|
|
bl_description = "Append inspected linked element"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def _execute(self, context):
|
|
from blenderbim.bim.module.project.data import LinksData
|
|
|
|
if not LinksData.linked_data:
|
|
self.report({"INFO"}, "No linked element found.")
|
|
return {"CANCELLED"}
|
|
|
|
guid = LinksData.linked_data["attributes"].get("GlobalId")
|
|
if guid is None:
|
|
self.report({"INFO"}, "Cannot find Global Id for element.")
|
|
return {"CANCELLED"}
|
|
|
|
queried_obj = context.scene.BIMProjectProperties.queried_obj
|
|
ifc_file = ifcopenshell.open(queried_obj["ifc_filepath"])
|
|
element_to_append = ifc_file.by_guid(guid)
|
|
element = ifcopenshell.api.run(
|
|
"project.append_asset",
|
|
tool.Ifc.get(),
|
|
library=ifc_file,
|
|
element=element_to_append,
|
|
)
|
|
self.import_product_from_ifc(element, context)
|
|
element_type = ifcopenshell.util.element.get_type(element)
|
|
obj = tool.Ifc.get_object(element_type)
|
|
if obj is None:
|
|
self.import_type_from_ifc(element_type, context)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class EnableCulling(bpy.types.Operator):
|
|
bl_idname = "bim.enable_culling"
|
|
bl_label = "Enable Culling"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def __init__(self):
|
|
self.last_view_corners = None
|
|
self.total_mousemoves = 0
|
|
self.cullable_objects = []
|
|
|
|
def modal(self, context, event):
|
|
if not LinksData.enable_culling:
|
|
for obj in bpy.context.visible_objects:
|
|
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
|
obj.display_type = "SOLID"
|
|
self.cullable_objects = []
|
|
return {"CANCELLED"}
|
|
|
|
# Even if the view is changing, there are certain scenarios where we
|
|
# don't want to apply culling. For example, if we scroll to zoom but
|
|
# are simultaneously moving our mouse, or if we `Zoom to Selected`. A
|
|
# dumb but seemingly effective way is to count MOUSEMOVE events. If at
|
|
# least 3 consecutive events occur, you're probably not doing some
|
|
# other navigational thing.
|
|
if event.type == "MOUSEMOVE":
|
|
self.total_mousemoves += 1
|
|
else:
|
|
self.total_mousemoves = 0
|
|
|
|
if self.total_mousemoves > 2 and self.is_view_changed(context):
|
|
self.total_mousemoves = 0
|
|
camera_position = context.region_data.view_matrix.inverted().translation
|
|
for obj in self.cullable_objects:
|
|
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
|
if self.is_object_in_view(obj, context, camera_position):
|
|
obj.display_type = "SOLID"
|
|
elif obj.display_type != "BOUNDS":
|
|
obj.display_type = "BOUNDS"
|
|
|
|
return {"PASS_THROUGH"}
|
|
|
|
def is_view_changed(self, context):
|
|
view_matrix = context.region_data.view_matrix
|
|
projection_matrix = context.region_data.window_matrix
|
|
vp_matrix = projection_matrix @ view_matrix
|
|
|
|
# Get NDC coordinates of the viewport corners
|
|
viewport_corners = [Vector((-1, -1, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)), Vector((-1, 1, 0))]
|
|
ndc_corners = [vp_matrix @ Vector((corner.x, corner.y, -1, 1)) for corner in viewport_corners]
|
|
ndc_corners = [(corner / corner.w).xy for corner in ndc_corners]
|
|
|
|
if self.last_view_corners != ndc_corners:
|
|
self.last_view_corners = ndc_corners
|
|
return True
|
|
return False
|
|
|
|
def is_object_in_view(self, obj, context, camera_position):
|
|
# Get the view matrix and the projection matrix from the active viewport
|
|
view_matrix = context.region_data.view_matrix
|
|
projection_matrix = context.region_data.window_matrix
|
|
# Calculate the combined view projection matrix
|
|
vp_matrix = projection_matrix @ view_matrix
|
|
obj_matrix_world = obj.matrix_world
|
|
|
|
# Transform each corner of the bounding box using the view projection matrix
|
|
# and check if it's inside the normalized device coordinates (NDC) space
|
|
for corner in [obj_matrix_world @ Vector(corner) for corner in obj.bound_box]:
|
|
ndc = vp_matrix @ corner.to_4d()
|
|
ndc /= ndc.w
|
|
if -1 <= ndc.x <= 1 and -1 <= ndc.y <= 1 and 0 <= ndc.z <= 1:
|
|
# At least one corner is inside the view, so the object is visible
|
|
break
|
|
else:
|
|
return False
|
|
|
|
# Check if the object is too far away from the camera
|
|
object_center = obj.matrix_world.translation
|
|
distance_threshold = 900 # 30m squared
|
|
if (camera_position - object_center).length_squared > distance_threshold:
|
|
# The object is too far away, so consider it not visible
|
|
return False
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
LinksData.enable_culling = True
|
|
self.cullable_objects = []
|
|
for obj in bpy.context.visible_objects:
|
|
if obj.type == "MESH" and obj.name.startswith("Ifc") and max(obj.dimensions) < 0.6:
|
|
self.cullable_objects.append(obj)
|
|
camera_position = context.region_data.view_matrix.inverted().translation
|
|
if not self.is_object_in_view(obj, context, camera_position):
|
|
obj.display_type = "BOUNDS"
|
|
context.window_manager.modal_handler_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class DisableCulling(bpy.types.Operator):
|
|
bl_idname = "bim.disable_culling"
|
|
bl_label = "Disable Culling"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
LinksData.enable_culling = False
|
|
return {"FINISHED"}
|
|
|
|
|
|
class RefreshClippingPlanes(bpy.types.Operator):
|
|
bl_idname = "bim.refresh_clipping_planes"
|
|
bl_label = "Refresh Clipping Planes"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def __init__(self):
|
|
self.total_planes = 0
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.modal_handler_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def modal(self, context, event):
|
|
should_refresh = False
|
|
|
|
self.clean_deleted_planes(context)
|
|
|
|
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
|
|
if clipping_plane.obj and self.is_moved(clipping_plane.obj):
|
|
should_refresh = True
|
|
break
|
|
|
|
total_planes = len(context.scene.BIMProjectProperties.clipping_planes)
|
|
if should_refresh or total_planes != self.total_planes:
|
|
self.refresh_clipping_planes(context)
|
|
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
|
|
if clipping_plane.obj:
|
|
tool.Geometry.record_object_position(clipping_plane.obj)
|
|
self.total_planes = total_planes
|
|
return {"PASS_THROUGH"}
|
|
|
|
def clean_deleted_planes(self, context):
|
|
while True:
|
|
for i, clipping_plane in enumerate(context.scene.BIMProjectProperties.clipping_planes):
|
|
if clipping_plane.obj:
|
|
try:
|
|
clipping_plane.obj.name
|
|
except:
|
|
context.scene.BIMProjectProperties.clipping_planes.remove(i)
|
|
break
|
|
else:
|
|
context.scene.BIMProjectProperties.clipping_planes.remove(i)
|
|
break
|
|
else:
|
|
break
|
|
|
|
def is_moved(self, obj):
|
|
if not obj.BIMObjectProperties.location_checksum:
|
|
return True # Let's be conservative
|
|
loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum))
|
|
rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum))
|
|
loc_real = np.array(obj.matrix_world.translation).flatten()
|
|
rot_real = np.array(obj.matrix_world.to_3x3()).flatten()
|
|
if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2):
|
|
return False
|
|
return True
|
|
|
|
def refresh_clipping_planes(self, context):
|
|
import bmesh
|
|
from itertools import cycle
|
|
|
|
area = next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D")
|
|
region = next(r for r in area.regions if r.type == "WINDOW")
|
|
data = region.data
|
|
|
|
if not len(context.scene.BIMProjectProperties.clipping_planes):
|
|
data.use_clip_planes = False
|
|
else:
|
|
with bpy.context.temp_override(area=area, region=region):
|
|
bpy.ops.view3d.clip_border()
|
|
|
|
clip_planes = []
|
|
for clipping_plane in bpy.context.scene.BIMProjectProperties.clipping_planes:
|
|
obj = clipping_plane.obj
|
|
if not obj:
|
|
continue
|
|
print("doing", obj)
|
|
|
|
bm = bmesh.new()
|
|
bm.from_mesh(obj.data)
|
|
|
|
world_matrix = obj.matrix_world
|
|
|
|
bm.faces.ensure_lookup_table()
|
|
face = bm.faces[0]
|
|
center = world_matrix @ face.calc_center_median()
|
|
print("center", center)
|
|
normal = world_matrix.to_3x3() @ face.normal * -1
|
|
center += normal * -0.01
|
|
print("normal", normal)
|
|
print("new center", center)
|
|
|
|
normal.normalize()
|
|
distance = -center.dot(normal)
|
|
clip_plane = (normal.x, normal.y, normal.z, distance)
|
|
clip_planes.append(clip_plane)
|
|
bm.free()
|
|
|
|
clip_planes = cycle(clip_planes)
|
|
data.clip_planes = [tuple(next(clip_planes)) for i in range(0, 6)]
|
|
data.update()
|
|
region.tag_redraw()
|
|
[a.tag_redraw() for a in bpy.context.screen.areas]
|
|
return {"FINISHED"}
|
|
|
|
|
|
class CreateClippingPlane(bpy.types.Operator):
|
|
bl_idname = "bim.create_clipping_plane"
|
|
bl_label = "Create Clipping Plane"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
|
|
|
|
# Clean up deleted planes
|
|
|
|
if len(context.scene.BIMProjectProperties.clipping_planes) > 5:
|
|
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
|
|
return {"FINISHED"}
|
|
|
|
for area in context.screen.areas:
|
|
if area.type == "VIEW_3D":
|
|
area.tag_redraw()
|
|
|
|
region = context.region
|
|
rv3d = context.region_data
|
|
if rv3d: # Called from a 3D viewport
|
|
coord = (self.mouse_x, self.mouse_y)
|
|
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
|
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
|
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
|
|
if not hit:
|
|
self.report({"INFO"}, "No object found.")
|
|
return {"FINISHED"}
|
|
else: # Not Called from a 3D viewport
|
|
location = (0, 0, 1)
|
|
normal = (0, 0, 1)
|
|
|
|
vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)]
|
|
|
|
faces = [(0, 1, 2, 3)]
|
|
|
|
mesh = bpy.data.meshes.new(name="ClippingPlane")
|
|
mesh.from_pydata(vertices, [], faces)
|
|
mesh.update()
|
|
|
|
plane_obj = bpy.data.objects.new("ClippingPlane", mesh)
|
|
context.collection.objects.link(plane_obj)
|
|
z_axis = Vector((0, 0, 1))
|
|
rotation_matrix = z_axis.rotation_difference(normal).to_matrix().to_4x4()
|
|
plane_obj.matrix_world = rotation_matrix
|
|
plane_obj.matrix_world.translation = location
|
|
|
|
context.scene.cursor.location = location
|
|
|
|
new = context.scene.BIMProjectProperties.clipping_planes.add()
|
|
new.obj = plane_obj
|
|
|
|
tool.Blender.set_active_object(plane_obj)
|
|
|
|
ClippingPlaneDecorator.install(context)
|
|
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
|
return {"FINISHED"}
|
|
|
|
def ray_cast(self, context, origin, direction):
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
result = context.scene.ray_cast(depsgraph, origin, direction)
|
|
return result
|
|
|
|
def invoke(self, context, event):
|
|
self.mouse_x = event.mouse_region_x
|
|
self.mouse_y = event.mouse_region_y
|
|
return self.execute(context)
|
|
|
|
|
|
class FlipClippingPlane(bpy.types.Operator):
|
|
bl_idname = "bim.flip_clipping_plane"
|
|
bl_label = "Flip Clipping Plane"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.active_object
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
if obj in context.scene.BIMProjectProperties.clipping_planes_objs:
|
|
obj.rotation_euler[0] += radians(180)
|
|
context.view_layer.update()
|
|
return {"FINISHED"}
|
|
|
|
|
|
CLIPPING_PLANES_FILE_NAME = "ClippingPlanes.json" # TODO un-hardcode :=
|
|
|
|
|
|
class BIM_OT_save_clipping_planes(bpy.types.Operator):
|
|
bl_idname = "bim.save_clipping_planes"
|
|
bl_label = "Save Clipping Planes"
|
|
bl_description = "Save Clipping Planes to Disk"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if IfcStore.path:
|
|
return context.scene.BIMProjectProperties.clipping_planes
|
|
cls.poll_message_set("Please Save The IFC File")
|
|
|
|
def execute(self, context):
|
|
clipping_planes_to_serialize = defaultdict(dict)
|
|
clipping_planes = context.scene.BIMProjectProperties.clipping_planes
|
|
for clipping_plane in clipping_planes:
|
|
obj = clipping_plane.obj
|
|
name = obj.name
|
|
clipping_planes_to_serialize[name]["location"] = obj.location[0:3]
|
|
clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3]
|
|
with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "w") as file:
|
|
json.dump(clipping_planes_to_serialize, file, indent=4)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class BIM_OT_load_clipping_planes(bpy.types.Operator):
|
|
bl_idname = "bim.load_clipping_planes"
|
|
bl_label = "Load Clipping Planes"
|
|
bl_description = "Load Clipping Planes from Disk"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if filepath := IfcStore.path:
|
|
if Path(filepath).with_name(CLIPPING_PLANES_FILE_NAME).exists():
|
|
return True
|
|
else:
|
|
cls.poll_message_set(f"No Clipping Planes File in Folder {filepath}")
|
|
else:
|
|
cls.poll_message_set("Please Save The IFC File")
|
|
|
|
def execute(self, context):
|
|
bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs)
|
|
context.scene.BIMProjectProperties.clipping_planes.clear()
|
|
with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file:
|
|
clipping_planes_dict = json.load(file)
|
|
for name, values in clipping_planes_dict.items():
|
|
bpy.ops.bim.create_clipping_plane()
|
|
obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1]
|
|
obj.name = name
|
|
obj.location = values["location"]
|
|
obj.rotation_euler = values["rotation"]
|
|
return {"FINISHED"}
|
|
|
|
|
|
if bpy.app.version >= (4, 1, 0):
|
|
|
|
class IFCFileHandlerOperator(bpy.types.Operator):
|
|
bl_idname = "bim.load_project_file_handler"
|
|
bl_label = "Import .ifc file"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
|
|
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
|
|
|
|
def invoke(self, context, event):
|
|
# Keeping code in .invoke() as we'll probably add some
|
|
# popup windows later.
|
|
|
|
# `files` contain only .ifc files.
|
|
filepath = Path(self.directory)
|
|
# If user is just drag'n'dropping a single file -> load it as a new project,
|
|
# if they're holding ALT -> link the file/files to the current project.
|
|
if event.alt:
|
|
# Passing self.files directly results in TypeError.
|
|
serialized_files = [{"name": f.name} for f in self.files]
|
|
return bpy.ops.bim.link_ifc(directory=self.directory, files=serialized_files)
|
|
else:
|
|
if len(self.files) == 1:
|
|
return bpy.ops.bim.load_project(filepath=(filepath / self.files[0].name).as_posix())
|
|
else:
|
|
self.report(
|
|
{"INFO"},
|
|
"To link multiple IFC files hold ALT while drag'n'dropping them.",
|
|
)
|
|
return {"FINISHED"}
|
|
|
|
class BIM_FH_import_ifc(bpy.types.FileHandler):
|
|
bl_label = "IFC File Handler"
|
|
bl_import_operator = IFCFileHandlerOperator.bl_idname
|
|
bl_file_extensions = ".ifc"
|
|
|
|
# FileHandler won't work without poll_drop defined.
|
|
@classmethod
|
|
def poll_drop(cls, context):
|
|
return True
|