mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 18:22:30 +00:00
Track last actions and friendlier error reporting when something breaks
This commit is contained in:
@@ -24,6 +24,7 @@ import traceback
|
|||||||
import subprocess
|
import subprocess
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import addon_utils
|
import addon_utils
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "BlenderBIM",
|
"name": "BlenderBIM",
|
||||||
@@ -38,6 +39,7 @@ bl_info = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
last_error = None
|
last_error = None
|
||||||
|
last_actions: deque = deque(maxlen=10)
|
||||||
|
|
||||||
|
|
||||||
def get_debug_info():
|
def get_debug_info():
|
||||||
@@ -60,10 +62,22 @@ def get_debug_info():
|
|||||||
"processor": platform.processor(),
|
"processor": platform.processor(),
|
||||||
"blender_version": bpy.app.version_string,
|
"blender_version": bpy.app.version_string,
|
||||||
"blenderbim_version": version,
|
"blenderbim_version": version,
|
||||||
|
"last_actions": last_actions,
|
||||||
"last_error": last_error,
|
"last_error": last_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_debug_info(info: dict):
|
||||||
|
last_actions = ""
|
||||||
|
for action in info["last_actions"]:
|
||||||
|
last_actions += f"\n# {action['type']}: {action['name']}"
|
||||||
|
if settings := action.get("settings"):
|
||||||
|
last_actions += f"\n>>> {settings}"
|
||||||
|
info["last_actions"] = last_actions
|
||||||
|
text = "\n".join(f"{k}: {v}" for k, v in info.items())
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
if sys.modules.get("bpy", None):
|
if sys.modules.get("bpy", None):
|
||||||
# Process *.pth in /libs/site/packages to setup globally importable modules
|
# Process *.pth in /libs/site/packages to setup globally importable modules
|
||||||
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
|
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
|
||||||
@@ -72,6 +86,18 @@ if sys.modules.get("bpy", None):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import blenderbim.bim
|
import blenderbim.bim
|
||||||
|
import ifcopenshell.api
|
||||||
|
|
||||||
|
def log_api(usecase_path, ifc_file, settings):
|
||||||
|
last_actions.append(
|
||||||
|
{
|
||||||
|
"type": "ifcopenshell.api",
|
||||||
|
"name": usecase_path,
|
||||||
|
"settings": ifcopenshell.api.serialise_settings(settings),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
blenderbim.bim.register()
|
blenderbim.bim.register()
|
||||||
@@ -83,7 +109,7 @@ if sys.modules.get("bpy", None):
|
|||||||
last_error = traceback.format_exc()
|
last_error = traceback.format_exc()
|
||||||
|
|
||||||
print(last_error)
|
print(last_error)
|
||||||
print(get_debug_info())
|
print(format_debug_info(get_debug_info()))
|
||||||
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
|
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
|
||||||
|
|
||||||
class BIM_PT_fatal_error(bpy.types.Panel):
|
class BIM_PT_fatal_error(bpy.types.Panel):
|
||||||
@@ -122,21 +148,14 @@ if sys.modules.get("bpy", None):
|
|||||||
bl_description = "Copies debugging information to your clipboard for use in bugreports"
|
bl_description = "Copies debugging information to your clipboard for use in bugreports"
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
info = get_debug_info()
|
info = format_debug_info(get_debug_info())
|
||||||
# Format it in a readable way
|
|
||||||
text = "\n".join(f"{k}: {v}" for k, v in info.items())
|
|
||||||
print(text)
|
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
command = "echo | set /p nul=" + text.strip()
|
command = "echo | set /p nul=" + info
|
||||||
elif platform.system() == "Darwin": # for MacOS
|
elif platform.system() == "Darwin": # for MacOS
|
||||||
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
|
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
|
||||||
else: # Linux
|
else: # Linux
|
||||||
command = (
|
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
|
||||||
'printf "'
|
|
||||||
+ text.strip().replace("\n", "\\n").replace('"', "")
|
|
||||||
+ '" | xclip -selection clipboard'
|
|
||||||
)
|
|
||||||
subprocess.run(command, shell=True, check=True)
|
subprocess.run(command, shell=True, check=True)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ import uuid
|
|||||||
import hashlib
|
import hashlib
|
||||||
import zipfile
|
import zipfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import traceback
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.geom
|
import ifcopenshell.geom
|
||||||
import ifcopenshell.ifcopenshell_wrapper
|
import ifcopenshell.ifcopenshell_wrapper
|
||||||
|
import blenderbim
|
||||||
import blenderbim.bim.handler
|
import blenderbim.bim.handler
|
||||||
import blenderbim.tool as tool
|
import blenderbim.tool as tool
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -37,19 +39,19 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
|||||||
|
|
||||||
class IfcStore:
|
class IfcStore:
|
||||||
path: str = ""
|
path: str = ""
|
||||||
file: ifcopenshell.file = None
|
file: Optional[ifcopenshell.file] = None
|
||||||
schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None
|
schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
|
||||||
cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None
|
cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
|
||||||
cache_path: str = None
|
cache_path: Optional[str] = None
|
||||||
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
|
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
|
||||||
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
|
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
|
||||||
edited_objs: Set[bpy.types.Object] = set()
|
edited_objs: Set[bpy.types.Object] = set()
|
||||||
pset_template_path: str = ""
|
pset_template_path: str = ""
|
||||||
pset_template_file: ifcopenshell.file = None
|
pset_template_file: Optional[ifcopenshell.file] = None
|
||||||
classification_path: str = ""
|
classification_path: str = ""
|
||||||
classification_file: ifcopenshell.file = None
|
classification_file: Optional[ifcopenshell.file] = None
|
||||||
library_path: str = ""
|
library_path: str = ""
|
||||||
library_file: ifcopenshell.file = None
|
library_file: Optional[ifcopenshell.file] = None
|
||||||
current_transaction = ""
|
current_transaction = ""
|
||||||
last_transaction = ""
|
last_transaction = ""
|
||||||
history = []
|
history = []
|
||||||
@@ -329,6 +331,7 @@ class IfcStore:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
|
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
|
||||||
|
blenderbim.last_actions.append({"type": "operator", "name": operator.bl_idname})
|
||||||
bpy.context.scene.BIMProperties.is_dirty = True
|
bpy.context.scene.BIMProperties.is_dirty = True
|
||||||
is_top_level_operator = not bool(IfcStore.current_transaction)
|
is_top_level_operator = not bool(IfcStore.current_transaction)
|
||||||
|
|
||||||
@@ -343,10 +346,14 @@ class IfcStore:
|
|||||||
else:
|
else:
|
||||||
operator.transaction_key = IfcStore.current_transaction
|
operator.transaction_key = IfcStore.current_transaction
|
||||||
|
|
||||||
if is_invoke:
|
try:
|
||||||
result = getattr(operator, "_invoke")(context, None)
|
if is_invoke:
|
||||||
else:
|
result = getattr(operator, "_invoke")(context, None)
|
||||||
result = getattr(operator, "_execute")(context)
|
else:
|
||||||
|
result = getattr(operator, "_execute")(context)
|
||||||
|
except:
|
||||||
|
blenderbim.last_error = traceback.format_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
if is_top_level_operator:
|
if is_top_level_operator:
|
||||||
if tool.Ifc.get():
|
if tool.Ifc.get():
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import blenderbim.tool as tool
|
|||||||
import blenderbim.core.debug as core
|
import blenderbim.core.debug as core
|
||||||
import blenderbim.bim.handler
|
import blenderbim.bim.handler
|
||||||
import blenderbim.bim.import_ifc as import_ifc
|
import blenderbim.bim.import_ifc as import_ifc
|
||||||
from blenderbim import get_debug_info
|
from blenderbim import get_debug_info, format_debug_info
|
||||||
from blenderbim.bim.ifc import IfcStore
|
from blenderbim.bim.ifc import IfcStore
|
||||||
|
|
||||||
|
|
||||||
@@ -54,16 +54,18 @@ class CopyDebugInformation(bpy.types.Operator):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Format it in a readable way
|
text = format_debug_info(info)
|
||||||
text = "\n".join(f"{k}: {v}" for k, v in info.items())
|
|
||||||
|
print("-" * 80)
|
||||||
print(text)
|
print(text)
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
command = "echo | set /p nul=" + text.strip()
|
command = "echo | set /p nul=" + text
|
||||||
elif platform.system() == "Darwin": # for MacOS
|
elif platform.system() == "Darwin": # for MacOS
|
||||||
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
|
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
|
||||||
else: # Linux
|
else: # Linux
|
||||||
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
|
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
|
||||||
subprocess.run(command, shell=True, check=True)
|
subprocess.run(command, shell=True, check=True)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -295,10 +295,11 @@ def serialise_settings(settings):
|
|||||||
vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
|
vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
|
||||||
elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
|
elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
|
||||||
vcs_settings[key] = [serialise_entity_instance(i) for i in value]
|
vcs_settings[key] = [serialise_entity_instance(i) for i in value]
|
||||||
if "add_representation" in usecase_path:
|
else:
|
||||||
return ""
|
try:
|
||||||
elif "owner." in usecase_path:
|
vcs_settings[key] = str(value)
|
||||||
return ""
|
except:
|
||||||
|
vcs_settings[key] = "n/a"
|
||||||
try:
|
try:
|
||||||
return json.dumps(vcs_settings)
|
return json.dumps(vcs_settings)
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ class Facet:
|
|||||||
def __init__(self, *parameters):
|
def __init__(self, *parameters):
|
||||||
self.status = None
|
self.status = None
|
||||||
self.failures: list[FacetFailure] = []
|
self.failures: list[FacetFailure] = []
|
||||||
|
self.parameters = []
|
||||||
|
self.applicability_templates = []
|
||||||
for i, name in enumerate(self.parameters):
|
for i, name in enumerate(self.parameters):
|
||||||
setattr(self, name.replace("@", ""), parameters[i])
|
setattr(self, name.replace("@", ""), parameters[i])
|
||||||
|
|
||||||
@@ -101,8 +103,10 @@ class Facet:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def filter(
|
def filter(
|
||||||
self, ifc_file: ifcopenshell.file, elements: list[ifcopenshell.entity_instance]
|
self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]]
|
||||||
) -> list[ifcopenshell.entity_instance]:
|
) -> list[ifcopenshell.entity_instance]:
|
||||||
|
if not elements:
|
||||||
|
return []
|
||||||
return [e for e in elements if self(e)]
|
return [e for e in elements if self(e)]
|
||||||
|
|
||||||
def to_string(
|
def to_string(
|
||||||
@@ -133,6 +137,7 @@ class Facet:
|
|||||||
total_replacements += 1
|
total_replacements += 1
|
||||||
if total_replacements == total_variables:
|
if total_replacements == total_variables:
|
||||||
return template
|
return template
|
||||||
|
return "This facet cannot be interpreted"
|
||||||
|
|
||||||
def to_ids_value(self, parameter: Union[str, Restriction, list]) -> dict[str, Any]:
|
def to_ids_value(self, parameter: Union[str, Restriction, list]) -> dict[str, Any]:
|
||||||
if isinstance(parameter, str):
|
if isinstance(parameter, str):
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ class Ids:
|
|||||||
milestone=None,
|
milestone=None,
|
||||||
):
|
):
|
||||||
# Not part of the IDS spec, but very useful in practice
|
# Not part of the IDS spec, but very useful in practice
|
||||||
self.filepath = None
|
self.filepath: Optional[str] = None
|
||||||
self.filename = None
|
self.filename: Optional[str] = None
|
||||||
|
|
||||||
self.specifications: List[Specification] = []
|
self.specifications: List[Specification] = []
|
||||||
self.info = {}
|
self.info = {}
|
||||||
@@ -300,6 +300,7 @@ class Specification:
|
|||||||
return "optional"
|
return "optional"
|
||||||
elif self.maxOccurs == 0:
|
elif self.maxOccurs == 0:
|
||||||
return "prohibited"
|
return "prohibited"
|
||||||
|
return "required" # Fallback
|
||||||
|
|
||||||
def set_usage(self, usage: Cardinality) -> None:
|
def set_usage(self, usage: Cardinality) -> None:
|
||||||
if usage == "optional":
|
if usage == "optional":
|
||||||
|
|||||||
Reference in New Issue
Block a user