mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-20 20:22:09 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
@@ -130,12 +130,7 @@ classes = [
|
||||
prop.StrProperty,
|
||||
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
||||
operator.BIM_OT_attribute_search_values,
|
||||
operator.BIM_UL_tab_panels,
|
||||
operator.BIM_OT_toggle_panel_visibility,
|
||||
operator.BIM_OT_bookmark_panel,
|
||||
operator.BIM_OT_manage_tab_panels,
|
||||
operator.BIM_OT_manage_tab_visibility,
|
||||
operator.BIM_OT_toggle_tab_visibility,
|
||||
operator.BIM_OT_reset_ui_layout,
|
||||
prop.ObjProperty,
|
||||
prop.MultipleFileSelect,
|
||||
@@ -144,7 +139,7 @@ classes = [
|
||||
prop.BIMAreaProperties,
|
||||
prop.BIMTabProperties,
|
||||
prop.BIMTabVisibility, # Must be registered before BIMProperties
|
||||
prop.BIMPanelProperties, # Must be registered before BIMProperties
|
||||
prop.BIMPanelVisibility, # Must be registered before BIMProperties
|
||||
prop.BIMProperties,
|
||||
prop.IfcParameter,
|
||||
prop.PsetQto,
|
||||
@@ -157,6 +152,8 @@ classes = [
|
||||
prop.BIMSnapGroups,
|
||||
ui.BIM_UL_clipping_plane,
|
||||
ui.BIM_UL_generic,
|
||||
ui.BIM_UL_tab_visibilities,
|
||||
ui.BIM_UL_panel_visibilities,
|
||||
ui.DocPreferences,
|
||||
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
|
||||
ui.GizmoPreferencesWindow, # Register before GizmoPreferences
|
||||
@@ -318,10 +315,6 @@ def register():
|
||||
# RestrictedContext doesn't allow accessing scene attribute, postpone it for a bit.
|
||||
bpy.app.timers.register(tool.Blender.setup_user_data_dir, first_interval=0.1)
|
||||
|
||||
bpy.types.Scene.active_tab_name = bpy.props.StringProperty()
|
||||
bpy.types.Scene.tab_panels = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
|
||||
bpy.types.Scene.active_tab_panel_index = bpy.props.IntProperty()
|
||||
|
||||
|
||||
def unregister():
|
||||
global icons
|
||||
@@ -363,7 +356,3 @@ def unregister():
|
||||
tool.Blender.remove_scene_panel_override(panel)
|
||||
|
||||
bpy.app.translations.unregister("bonsai")
|
||||
|
||||
del bpy.types.Scene.active_tab_name
|
||||
del bpy.types.Scene.tab_panels
|
||||
del bpy.types.Scene.active_tab_panel_index
|
||||
|
||||
@@ -428,7 +428,11 @@ def get_enum_items(
|
||||
else:
|
||||
annotations_data = data
|
||||
|
||||
prop = annotations_data.__annotations__[prop_name]
|
||||
try:
|
||||
annotations = annotations_data.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(annotations_data).__annotations__
|
||||
prop = annotations[prop_name]
|
||||
items = prop.keywords.get("items")
|
||||
if items is None:
|
||||
return
|
||||
@@ -788,149 +792,3 @@ def draw_filter(
|
||||
op.group_index = i
|
||||
op.index = j
|
||||
op.module = module
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# UI Panel Visibility Helpers
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_tab_names():
|
||||
from bonsai.bim.prop import get_tab
|
||||
|
||||
enum_items = get_tab(None, None)
|
||||
# Exclude None separators and the BLENDER tab (not part of BIM tab system)
|
||||
return [item[0] for item in enum_items if item is not None and item[0] != "BLENDER"]
|
||||
|
||||
|
||||
def get_panel_tab_name(panel_class):
|
||||
if hasattr(panel_class, "bim_tab_name"):
|
||||
return panel_class.bim_tab_name
|
||||
return "PROJECT" # Default fallback
|
||||
|
||||
|
||||
def should_show_panel(panel_id, panel_tab_name, context):
|
||||
if tool.Blender.is_tab(context, "BOOKMARK"):
|
||||
return is_panel_bookmarked(panel_id) and get_panel_visibility(panel_id, "BOOKMARK")
|
||||
|
||||
if tool.Blender.is_tab(context, panel_tab_name):
|
||||
return get_tab_visibility(panel_tab_name) and get_panel_visibility(panel_id, panel_tab_name)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_tab_visibility(tab_name):
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
tab_vis = bim_props.tab_visibilities.get(tab_name)
|
||||
return tab_vis.is_visible if tab_vis else True
|
||||
|
||||
|
||||
def set_tab_visibility(tab_name, visible):
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
tab_vis = bim_props.tab_visibilities.get(tab_name)
|
||||
if tab_vis:
|
||||
tab_vis.is_visible = visible
|
||||
else:
|
||||
new_tab = bim_props.tab_visibilities.add()
|
||||
new_tab.name = tab_name
|
||||
new_tab.is_visible = visible
|
||||
|
||||
|
||||
def get_panel_visibility(panel_id, current_tab=None):
|
||||
panel_config = get_panel_config(panel_id)
|
||||
if panel_config:
|
||||
if current_tab == "BOOKMARK":
|
||||
return panel_config.is_visible_in_bookmarks
|
||||
else:
|
||||
return panel_config.is_visible_in_tab
|
||||
return True
|
||||
|
||||
|
||||
def is_panel_bookmarked(panel_id):
|
||||
panel_config = get_panel_config(panel_id)
|
||||
if panel_config:
|
||||
return panel_config.is_bookmarked
|
||||
return False
|
||||
|
||||
|
||||
def get_panel_config(panel_id, create_if_missing=False):
|
||||
try:
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
except (AttributeError, AssertionError):
|
||||
return None
|
||||
|
||||
for prop in bim_props.panel_properties:
|
||||
if prop.name == panel_id:
|
||||
return prop
|
||||
|
||||
if create_if_missing:
|
||||
try:
|
||||
prop = bim_props.panel_properties.add()
|
||||
prop.name = panel_id
|
||||
prop.is_visible_in_tab = True
|
||||
prop.is_visible_in_bookmarks = True
|
||||
prop.is_bookmarked = False
|
||||
return prop
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_all_tab_panels(force_refresh=False):
|
||||
panels = {tab_name: [] for tab_name in get_tab_names() if tab_name != "BOOKMARK"}
|
||||
panels["BOOKMARK"] = []
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
for prop in bim_props.panel_properties:
|
||||
panel_class = getattr(bpy.types, prop.name, None)
|
||||
if panel_class:
|
||||
tab_name = get_panel_tab_name(panel_class)
|
||||
if tab_name and tab_name != "BOOKMARK":
|
||||
bl_label = getattr(panel_class, "bl_label", prop.name)
|
||||
panels[tab_name].append({"bl_idname": prop.name, "bl_label": bl_label})
|
||||
|
||||
if prop.is_bookmarked:
|
||||
panel_class = getattr(bpy.types, prop.name, None)
|
||||
if panel_class:
|
||||
bl_label = getattr(panel_class, "bl_label", prop.name)
|
||||
panels["BOOKMARK"].append({"bl_idname": prop.name, "bl_label": bl_label})
|
||||
|
||||
if not panels["BOOKMARK"]:
|
||||
panels["BOOKMARK"] = [{}]
|
||||
|
||||
return panels
|
||||
|
||||
|
||||
def initialize_tab_visibilities():
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
|
||||
if len(bim_props.tab_visibilities) > 0:
|
||||
return
|
||||
|
||||
for tab_name in get_tab_names():
|
||||
tab_vis = bim_props.tab_visibilities.add()
|
||||
tab_vis.name = tab_name
|
||||
tab_vis.is_visible = True
|
||||
|
||||
|
||||
def initialize_panel_properties():
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
|
||||
if len(bim_props.panel_properties) > 0:
|
||||
return
|
||||
|
||||
for attr_name in dir(bpy.types):
|
||||
if attr_name.startswith("BIM_PT_tab_"):
|
||||
panel_class = getattr(bpy.types, attr_name)
|
||||
if not hasattr(panel_class, "bl_idname"):
|
||||
continue
|
||||
|
||||
panel_id = panel_class.bl_idname
|
||||
|
||||
prop = bim_props.panel_properties.add()
|
||||
prop.name = panel_id
|
||||
prop.is_visible_in_tab = True
|
||||
prop.is_visible_in_bookmarks = True
|
||||
prop.is_bookmarked = False
|
||||
|
||||
@@ -1295,6 +1295,7 @@ class IfcImporter:
|
||||
if element.is_a("IfcSpace"):
|
||||
obj.hide_set(True)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
"""
|
||||
Initialize only using `IfcImportSettings.factory()`.
|
||||
|
||||
@@ -24,26 +24,33 @@ classes = (
|
||||
operator.AddInformation,
|
||||
operator.AssignDocument,
|
||||
operator.DisableDocumentEditingUI,
|
||||
operator.DisableObjectDocumentEditingUI,
|
||||
operator.DisableEditingDocument,
|
||||
operator.EditDocument,
|
||||
operator.EnableEditingDocument,
|
||||
operator.LoadDocument,
|
||||
operator.LoadParentDocument,
|
||||
operator.LoadObjectDocuments,
|
||||
operator.LoadProjectDocuments,
|
||||
operator.RemoveDocument,
|
||||
operator.SelectDocumentObjects,
|
||||
operator.ToggleDocument,
|
||||
operator.UnassignDocument,
|
||||
operator.OpenIFCDocument,
|
||||
prop.Document,
|
||||
prop.DocumentObject,
|
||||
prop.BIMDocumentProperties,
|
||||
ui.BIM_PT_documents,
|
||||
ui.BIM_PT_object_documents,
|
||||
ui.BIM_UL_documents,
|
||||
ui.BIM_UL_document_objects,
|
||||
ui.BIM_MT_object_documents_context_menu,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMDocumentProperties = bpy.props.PointerProperty(type=prop.BIMDocumentProperties)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(ui.add_object_documents_context_menu)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMDocumentProperties
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.add_object_documents_context_menu)
|
||||
|
||||
@@ -21,6 +21,7 @@ import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.schema
|
||||
import bonsai.tool as tool
|
||||
from natsort import natsorted
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -35,30 +36,50 @@ class DocumentData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"total_information": cls.total_information(),
|
||||
"parent_document": cls.parent_document(),
|
||||
"total_documents": cls.total_documents(),
|
||||
"document_objects": cls.document_objects(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def total_information(cls):
|
||||
return len(
|
||||
[
|
||||
rel
|
||||
for rel in tool.Ifc.get().by_type("IfcProject")[0].HasAssociations or []
|
||||
if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation")
|
||||
]
|
||||
)
|
||||
def total_documents(cls):
|
||||
file = tool.Ifc.get()
|
||||
return len(file.by_type("IfcDocumentInformation")) + len(file.by_type("IfcDocumentReference"))
|
||||
|
||||
@classmethod
|
||||
def parent_document(cls):
|
||||
def document_objects(cls):
|
||||
document_objects = {}
|
||||
file = tool.Ifc.get()
|
||||
|
||||
for rel in file.by_type("IfcRelAssociatesDocument"):
|
||||
document_id = rel.RelatingDocument.id()
|
||||
if document_id not in document_objects:
|
||||
document_objects[document_id] = []
|
||||
|
||||
for related_object in rel.RelatedObjects:
|
||||
element = related_object
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
document_objects[document_id].append({"id": element.id(), "name": obj.name, "obj": obj})
|
||||
|
||||
return document_objects
|
||||
|
||||
@classmethod
|
||||
def load_document_objects_into_props(cls, document_id):
|
||||
if not cls.is_loaded:
|
||||
cls.load()
|
||||
|
||||
props = tool.Document.get_document_props()
|
||||
if len(props.breadcrumbs):
|
||||
parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return str(parent.DocumentId)
|
||||
return str(parent.Identification)
|
||||
return ""
|
||||
props.document_objects.clear()
|
||||
|
||||
if document_id not in cls.data["document_objects"]:
|
||||
return
|
||||
|
||||
sorted_objects = natsorted(cls.data["document_objects"][document_id], key=lambda x: x["name"].lower())
|
||||
|
||||
for obj_data in sorted_objects:
|
||||
item = props.document_objects.add()
|
||||
item.name = obj_data["name"]
|
||||
|
||||
|
||||
class ObjectDocumentData:
|
||||
@@ -72,6 +93,18 @@ class ObjectDocumentData:
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@staticmethod
|
||||
def convert_to_file_uri(location: str) -> str:
|
||||
if not location:
|
||||
return ""
|
||||
|
||||
uri = location
|
||||
if "://" not in uri:
|
||||
if not os.path.isabs(uri):
|
||||
uri = os.path.abspath(os.path.join(os.path.dirname(tool.Ifc.get_path()), uri))
|
||||
uri = "file://" + uri
|
||||
return uri
|
||||
|
||||
@classmethod
|
||||
def documents(cls):
|
||||
results = []
|
||||
@@ -80,44 +113,61 @@ class ObjectDocumentData:
|
||||
return results
|
||||
for rel in getattr(element, "HasAssociations", []):
|
||||
if rel.is_a("IfcRelAssociatesDocument"):
|
||||
if not rel.RelatingDocument.is_a("IfcDocumentReference"):
|
||||
relating_document = rel.RelatingDocument
|
||||
|
||||
is_information = relating_document.is_a("IfcDocumentInformation")
|
||||
is_reference = relating_document.is_a("IfcDocumentReference")
|
||||
|
||||
if not (is_information or is_reference):
|
||||
continue
|
||||
|
||||
name = rel.RelatingDocument.Name
|
||||
name = relating_document.Name
|
||||
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
if not name and rel.RelatingDocument.ReferenceToDocument:
|
||||
name = rel.RelatingDocument.ReferenceToDocument[0].Name
|
||||
location = None
|
||||
identification = None
|
||||
|
||||
identification = rel.RelatingDocument.ItemReference
|
||||
if not identification and rel.RelatingDocument.ReferenceToDocument:
|
||||
identification = rel.RelatingDocument.ReferenceToDocument[0].DocumentId
|
||||
if is_information:
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
identification = relating_document.DocumentId
|
||||
else:
|
||||
identification = relating_document.Identification
|
||||
|
||||
location = rel.RelatingDocument.Location
|
||||
location = getattr(relating_document, "Location", None)
|
||||
description = getattr(relating_document, "Description", "No description")
|
||||
else:
|
||||
if not name and rel.RelatingDocument.ReferencedDocument:
|
||||
name = rel.RelatingDocument.ReferencedDocument.Name
|
||||
description = relating_document.Description
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
reference_to_document = relating_document.ReferenceToDocument
|
||||
if not name and reference_to_document:
|
||||
name = reference_to_document[0].Name
|
||||
|
||||
identification = rel.RelatingDocument.Identification
|
||||
if not identification and rel.RelatingDocument.ReferencedDocument:
|
||||
identification = rel.RelatingDocument.ReferencedDocument.Identification
|
||||
identification = relating_document.ItemReference
|
||||
if not identification and reference_to_document:
|
||||
identification = reference_to_document[0].DocumentId
|
||||
location = relating_document.Location
|
||||
else:
|
||||
referenced_document = relating_document.ReferencedDocument
|
||||
if not name and referenced_document:
|
||||
name = referenced_document.Name
|
||||
|
||||
location = rel.RelatingDocument.Location
|
||||
if location is None and rel.RelatingDocument.ReferencedDocument:
|
||||
location = rel.RelatingDocument.ReferencedDocument.Location
|
||||
identification = relating_document.Identification
|
||||
if not identification and referenced_document:
|
||||
identification = referenced_document.Identification
|
||||
|
||||
if location:
|
||||
if not "://" in location:
|
||||
if not os.path.isabs(location):
|
||||
location = os.path.abspath(os.path.join(os.path.dirname(tool.Ifc.get_path()), location))
|
||||
location = "file://" + location
|
||||
location = relating_document.Location
|
||||
if location is None and referenced_document:
|
||||
location = referenced_document.Location
|
||||
|
||||
location = cls.convert_to_file_uri(location) if location else None
|
||||
|
||||
results.append(
|
||||
{
|
||||
"id": rel.RelatingDocument.id(),
|
||||
"id": relating_document.id(),
|
||||
"identification": identification,
|
||||
"name": name,
|
||||
"location": location,
|
||||
"is_information": is_information,
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -18,12 +18,10 @@
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.bim.handler
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.document as core
|
||||
from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData
|
||||
|
||||
|
||||
class LoadProjectDocuments(bpy.types.Operator):
|
||||
@@ -33,30 +31,6 @@ class LoadProjectDocuments(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
core.load_project_documents(tool.Document)
|
||||
bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data.
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadDocument(bpy.types.Operator):
|
||||
bl_idname = "bim.load_document"
|
||||
bl_label = "Load Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
core.load_document(tool.Document, document=tool.Ifc.get().by_id(self.document))
|
||||
bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data.
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadParentDocument(bpy.types.Operator):
|
||||
bl_idname = "bim.load_parent_document"
|
||||
bl_label = "Load Parent Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
core.load_parent_document(tool.Document)
|
||||
bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data.
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -70,6 +44,16 @@ class DisableDocumentEditingUI(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableObjectDocumentEditingUI(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_object_document_editing_ui"
|
||||
bl_label = "Disable Object Document Editing UI"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
core.disable_object_document_editing_ui(tool.Document)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingDocument(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_document"
|
||||
bl_label = "Enable Editing Document"
|
||||
@@ -77,7 +61,7 @@ class EnableEditingDocument(bpy.types.Operator):
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
core.enable_editing_document(tool.Document, document=tool.Ifc.get().by_id(self.document))
|
||||
core.enable_editing_document(tool.Document, ifc_document=tool.Ifc.get().by_id(self.document))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -97,7 +81,35 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.add_information(tool.Ifc, tool.Document)
|
||||
props = tool.Document.get_document_props()
|
||||
parent = None
|
||||
if props.active_document:
|
||||
selected_document = props.active_document
|
||||
|
||||
if selected_document.document_type == "PROJECT":
|
||||
parent = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
elif selected_document.document_type == "INFORMATION":
|
||||
parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id)
|
||||
elif selected_document.document_type == "REFERENCE":
|
||||
self.report({"ERROR"}, "Cannot add an information element as a child of a reference element")
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
parent = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
|
||||
core.add_information(tool.Ifc, tool.Document, parent)
|
||||
|
||||
expanded_docs = []
|
||||
try:
|
||||
expanded_docs = json.loads(props.json_string)
|
||||
except (AttributeError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
if parent and parent.is_a("IfcDocumentInformation"):
|
||||
if parent.id() not in expanded_docs:
|
||||
expanded_docs.append(parent.id())
|
||||
|
||||
props.json_string = json.dumps(expanded_docs)
|
||||
bpy.ops.bim.load_project_documents()
|
||||
|
||||
|
||||
class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -106,7 +118,33 @@ class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Document.get_document_props()
|
||||
|
||||
if not props.active_document:
|
||||
self.report({"ERROR"}, "No document selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
selected_document = props.active_document
|
||||
|
||||
if selected_document.document_type != "INFORMATION":
|
||||
self.report({"ERROR"}, "Cannot add a reference to a document that is not an information element")
|
||||
return {"CANCELLED"}
|
||||
|
||||
parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id)
|
||||
|
||||
props.document_attributes.clear()
|
||||
core.add_reference(tool.Ifc, tool.Document)
|
||||
expanded_docs = []
|
||||
try:
|
||||
expanded_docs = json.loads(props.json_string)
|
||||
except (AttributeError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
if parent.id() not in expanded_docs:
|
||||
expanded_docs.append(parent.id())
|
||||
props.json_string = json.dumps(expanded_docs)
|
||||
|
||||
bpy.ops.bim.load_project_documents()
|
||||
|
||||
|
||||
class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -116,7 +154,9 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Document.get_document_props()
|
||||
core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id))
|
||||
if props.active_document_id:
|
||||
core.edit_document(tool.Ifc, tool.Document, ifc_document=tool.Ifc.get().by_id(props.active_document_id))
|
||||
props.active_document_id = 0
|
||||
|
||||
|
||||
class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -126,7 +166,7 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(self.document))
|
||||
core.remove_document(tool.Ifc, tool.Document, ifc_document=tool.Ifc.get().by_id(self.document))
|
||||
|
||||
|
||||
class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -138,12 +178,15 @@ class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
document = tool.Ifc.get().by_id(self.document)
|
||||
objs = [bpy.data.objects[self.obj]] if self.obj else tool.Blender.get_selected_objects()
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
core.assign_document(tool.Ifc, product=element, document=document)
|
||||
core.assign_document(tool.Ifc, product=element, ifc_document=tool.Ifc.get().by_id(self.document))
|
||||
|
||||
tool.Document.update_document_objects(self.document)
|
||||
ObjectDocumentData.load()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -154,12 +197,25 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
document = tool.Ifc.get().by_id(self.document)
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
core.unassign_document(tool.Ifc, product=element, document=document)
|
||||
if obj:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
core.unassign_document(tool.Ifc, product=element, ifc_document=tool.Ifc.get().by_id(self.document))
|
||||
|
||||
props = tool.Document.get_document_props()
|
||||
active_document_id = None
|
||||
if props.active_document:
|
||||
active_document_id = props.active_document.ifc_definition_id
|
||||
|
||||
if active_document_id and active_document_id != self.document:
|
||||
tool.Document.update_document_objects(active_document_id)
|
||||
else:
|
||||
tool.Document.update_document_objects()
|
||||
|
||||
ObjectDocumentData.load()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectDocumentObjects(bpy.types.Operator):
|
||||
@@ -182,3 +238,81 @@ class SelectDocumentObjects(bpy.types.Operator):
|
||||
i += 1
|
||||
self.report({"INFO"}, f"{i} objects selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadObjectDocuments(bpy.types.Operator):
|
||||
bl_idname = "bim.load_object_documents"
|
||||
bl_label = "Load Object Documents"
|
||||
bl_description = "Load documents to assign to the selected object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
core.load_project_documents(tool.Document)
|
||||
|
||||
props = tool.Document.get_document_props()
|
||||
props.is_object_editing = True
|
||||
ObjectDocumentData.load()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenIFCDocument(bpy.types.Operator):
|
||||
bl_idname = "bim.open_ifc_document"
|
||||
bl_label = "Open IFC Document"
|
||||
bl_description = "Open the IFC document in a new Blender instance and load the project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
uri: bpy.props.StringProperty(name="URI")
|
||||
|
||||
def execute(self, context):
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
if not self.uri or not self.uri.lower().startswith("file://"):
|
||||
self.report({"ERROR"}, "Only local file:// URIs are supported")
|
||||
return {"CANCELLED"}
|
||||
|
||||
filepath = self.uri[7:]
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
self.report({"ERROR"}, f"File not found: {filepath}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
blender_path = bpy.app.binary_path
|
||||
args = [
|
||||
blender_path,
|
||||
"--python-expr",
|
||||
"import bpy; bpy.ops.bim.load_project(filepath='{}')".format(filepath),
|
||||
]
|
||||
subprocess.Popen(args)
|
||||
self.report({"INFO"}, f"Opening {filepath} in a new Blender instance")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleDocument(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_document"
|
||||
bl_label = "Toggle Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
document: bpy.props.IntProperty()
|
||||
option: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
expanded_documents = []
|
||||
props = tool.Document.get_document_props()
|
||||
try:
|
||||
expanded_documents = json.loads(props.json_string)
|
||||
except (AttributeError, json.JSONDecodeError):
|
||||
expanded_documents = []
|
||||
|
||||
document_id = self.document
|
||||
|
||||
document = tool.Ifc.get().by_id(document_id)
|
||||
if document:
|
||||
if self.option == "Expand" and document_id not in expanded_documents:
|
||||
expanded_documents.append(document_id)
|
||||
elif self.option == "Collapse" and document_id in expanded_documents:
|
||||
expanded_documents.remove(document_id)
|
||||
|
||||
props.json_string = json.dumps(expanded_documents)
|
||||
bpy.ops.bim.load_project_documents()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai 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.
|
||||
#
|
||||
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
from bonsai.bim.module.document.data import refresh
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -30,6 +13,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from bonsai.bim.module.document.data import DocumentData
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
||||
@@ -49,18 +33,50 @@ def update_document_identification(self: "Document", context: bpy.types.Context)
|
||||
tool.Document.set_external_reference_id(document, self.identification)
|
||||
|
||||
|
||||
def update_active_document_index(self, context):
|
||||
refresh()
|
||||
if document := self.active_document:
|
||||
if document.ifc_definition_id:
|
||||
DocumentData.load_document_objects_into_props(document.ifc_definition_id)
|
||||
|
||||
|
||||
class Document(PropertyGroup):
|
||||
name: StringProperty(name="Name", update=update_document_name)
|
||||
identification: StringProperty(name="Identification", update=update_document_identification)
|
||||
is_information: BoolProperty(
|
||||
name="Is Information",
|
||||
description="Whether element is IfcDocumentInformation, otherwise it's IfcDocumentReference.",
|
||||
name: StringProperty(name="Name")
|
||||
identification: StringProperty(name="Identification")
|
||||
description: StringProperty(name="Description")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
location: StringProperty(name="Location", default="")
|
||||
tree_depth: IntProperty(name="Tree Depth", default=0)
|
||||
has_children: BoolProperty(name="Has Children", default=False)
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=False)
|
||||
document_type: EnumProperty(
|
||||
name="Document Type",
|
||||
items=[
|
||||
("PROJECT", "Project", "Virtual project root node"),
|
||||
("INFORMATION", "Information", "IfcDocumentInformation"),
|
||||
("REFERENCE", "Reference", "IfcDocumentReference"),
|
||||
],
|
||||
default="INFORMATION",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
identification: str
|
||||
description: str
|
||||
ifc_definition_id: int
|
||||
location: str
|
||||
tree_depth: int
|
||||
has_children: bool
|
||||
is_expanded: bool
|
||||
document_type: str
|
||||
|
||||
|
||||
class DocumentObject(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
identification: str
|
||||
is_information: bool
|
||||
name: str
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
@@ -68,17 +84,23 @@ class BIMDocumentProperties(PropertyGroup):
|
||||
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
|
||||
active_document_id: IntProperty(name="Active Document Id")
|
||||
documents: CollectionProperty(name="Documents", type=Document)
|
||||
breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty)
|
||||
active_document_index: IntProperty(name="Active Document Index")
|
||||
active_document_index: IntProperty(name="Active Document Index", update=update_active_document_index)
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
is_object_editing: BoolProperty(name="Is Object Editing", default=False)
|
||||
document_objects: CollectionProperty(name="Document Objects", type=DocumentObject)
|
||||
active_document_object_index: IntProperty(name="Active Document Object Index")
|
||||
json_string: StringProperty(name="JSON String", default="[]")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
active_document_id: int
|
||||
documents: bpy.types.bpy_prop_collection_idprop[Document]
|
||||
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
active_document_index: int
|
||||
is_editing: bool
|
||||
is_object_editing: bool
|
||||
document_objects: bpy.types.bpy_prop_collection_idprop[DocumentObject]
|
||||
active_document_object_index: int
|
||||
json_string: str
|
||||
|
||||
@property
|
||||
def active_document(self) -> Union[Document, None]:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.helper import draw_attributes
|
||||
@@ -42,7 +43,8 @@ class BIM_PT_documents(Panel):
|
||||
self.props = tool.Document.get_document_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE")
|
||||
row.label(text="{} Documents found".format(DocumentData.data["total_documents"]), icon="FILE")
|
||||
|
||||
if self.props.is_editing:
|
||||
row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL")
|
||||
else:
|
||||
@@ -52,34 +54,54 @@ class BIM_PT_documents(Panel):
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
if self.props.breadcrumbs:
|
||||
row.operator("bim.load_parent_document", text="", icon="FRAME_PREV")
|
||||
row.label(text=DocumentData.data["parent_document"])
|
||||
else:
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.add_information", text="", icon="ADD")
|
||||
if self.props.breadcrumbs:
|
||||
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
active_document = self.props.active_document
|
||||
|
||||
if self.props.active_document_id:
|
||||
if self.props.active_document_id > 0:
|
||||
row.operator("bim.edit_document", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
|
||||
elif active_document:
|
||||
ifc_definition_id = active_document.ifc_definition_id
|
||||
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
|
||||
ifc_definition_id
|
||||
)
|
||||
row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id
|
||||
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
|
||||
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
|
||||
else:
|
||||
if not self.props.active_document or self.props.active_document.document_type in ["INFORMATION", "PROJECT"]:
|
||||
row.operator("bim.add_information", text="", icon="ADD")
|
||||
|
||||
if self.props.active_document and (
|
||||
self.props.active_document.document_type == "INFORMATION"
|
||||
and self.props.active_document.document_type != "PROJECT"
|
||||
):
|
||||
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
|
||||
|
||||
active_document = self.props.active_document
|
||||
if active_document:
|
||||
ifc_definition_id = active_document.ifc_definition_id
|
||||
|
||||
if active_document.document_type != "PROJECT":
|
||||
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
|
||||
ifc_definition_id
|
||||
)
|
||||
row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id
|
||||
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = (
|
||||
ifc_definition_id
|
||||
)
|
||||
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
|
||||
self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index")
|
||||
|
||||
if self.props.active_document_id:
|
||||
if self.props.active_document_id > 0:
|
||||
active_document = self.props.active_document
|
||||
draw_attributes(self.props.document_attributes, self.layout)
|
||||
|
||||
if self.props.is_editing and self.props.active_document:
|
||||
document = self.props.active_document
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
row.label(text="Assigned Objects", icon="OUTLINER_OB_EMPTY")
|
||||
box.template_list(
|
||||
"BIM_UL_document_objects",
|
||||
"",
|
||||
self.props,
|
||||
"document_objects",
|
||||
self.props,
|
||||
"active_document_object_index",
|
||||
)
|
||||
|
||||
|
||||
class BIM_PT_object_documents(Panel):
|
||||
bl_label = "Documents"
|
||||
@@ -102,65 +124,209 @@ class BIM_PT_object_documents(Panel):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
if not ObjectDocumentData.is_loaded:
|
||||
ObjectDocumentData.load()
|
||||
|
||||
obj = context.active_object
|
||||
self.oprops = tool.Blender.get_object_bim_props(obj)
|
||||
self.props = tool.Document.get_document_props()
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
self.draw_add_ui()
|
||||
|
||||
if not ObjectDocumentData.data["documents"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Documents", icon="FILE")
|
||||
|
||||
for document in ObjectDocumentData.data["documents"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=document["identification"] or "*", icon="FILE")
|
||||
row.label(text=document["name"] or "Unnamed")
|
||||
if document["location"]:
|
||||
row.operator("bim.open_uri", icon="URL", text="").uri = document["location"]
|
||||
row.operator("bim.unassign_document", text="", icon="X").document = document["id"]
|
||||
|
||||
def draw_add_ui(self):
|
||||
if not self.props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.load_project_documents", text="Assign Document References", icon="ADD")
|
||||
return
|
||||
doc_count = len(ObjectDocumentData.data["documents"])
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
if self.props.breadcrumbs:
|
||||
row.operator("bim.load_parent_document", text="", icon="FRAME_PREV")
|
||||
row.label(text=DocumentData.data["parent_document"])
|
||||
row.label(text="{} Documents Assigned".format(doc_count), icon="FILE")
|
||||
|
||||
if self.props.is_object_editing:
|
||||
row.operator("bim.disable_object_document_editing_ui", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.load_object_documents", text="", icon="IMPORT")
|
||||
|
||||
if not self.props.is_object_editing and doc_count == 0:
|
||||
row = self.layout.row()
|
||||
row.label(text="No documents assigned", icon="INFO")
|
||||
return
|
||||
|
||||
if self.props.is_object_editing:
|
||||
self.draw_add_ui()
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
row.label(text="Assigned Documents", icon="OUTLINER_OB_EMPTY")
|
||||
|
||||
if doc_count > 0:
|
||||
col = box.column(align=True)
|
||||
for document in ObjectDocumentData.data["documents"]:
|
||||
row = col.row(align=True)
|
||||
|
||||
# Create a split layout to separate left and right sides
|
||||
split = row.split(factor=0.7) # Adjust factor as needed (0.7 = 70% left, 30% right)
|
||||
|
||||
# Left side - Document identification and name
|
||||
left_side = split.row(align=True)
|
||||
left_side.alignment = "LEFT"
|
||||
left_side.label(text=document["identification"] or "*", icon="FILE")
|
||||
left_side.label(text=document["name"] or "Unnamed")
|
||||
|
||||
# Right side - Action buttons
|
||||
right_side = split.row(align=True)
|
||||
right_side.alignment = "RIGHT" # Align buttons to the right
|
||||
|
||||
if document["location"]:
|
||||
if document["location"].lower().endswith(".ifc"):
|
||||
right_side.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = document[
|
||||
"location"
|
||||
]
|
||||
right_side.operator("bim.open_uri", icon="URL", text="").uri = document["location"]
|
||||
|
||||
right_side.operator("bim.unassign_document", text="", icon="X").document = document["id"]
|
||||
|
||||
def draw_add_ui(self):
|
||||
if self.props.is_object_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
if self.props.documents and self.props.active_document_index < len(self.props.documents):
|
||||
document = self.props.documents[self.props.active_document_index]
|
||||
if not document.is_information:
|
||||
row.operator("bim.assign_document", text="", icon="ADD").document = document.ifc_definition_id
|
||||
row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL")
|
||||
if self.props.active_document:
|
||||
document = self.props.active_document
|
||||
|
||||
self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index")
|
||||
assigned_doc_ids = []
|
||||
for doc in ObjectDocumentData.data["documents"]:
|
||||
assigned_doc_ids.append(doc["id"])
|
||||
|
||||
if (
|
||||
document.document_type == "INFORMATION"
|
||||
and document.document_type != "PROJECT"
|
||||
and document.ifc_definition_id not in assigned_doc_ids
|
||||
):
|
||||
doc_op = row.operator("bim.assign_document", text="", icon="BRUSH_DATA")
|
||||
doc_op.document = document.ifc_definition_id
|
||||
elif document.ifc_definition_id in assigned_doc_ids:
|
||||
row.label(text="", icon="CHECKMARK")
|
||||
self.layout.template_list(
|
||||
"BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index"
|
||||
)
|
||||
|
||||
|
||||
class BIM_UL_documents(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
indent_depth = 0
|
||||
|
||||
if item.is_information:
|
||||
op = row.operator("bim.load_document", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT")
|
||||
op.document = item.ifc_definition_id
|
||||
row.label(text="", icon="FILE")
|
||||
else:
|
||||
if item.document_type != "PROJECT":
|
||||
if item.tree_depth > 1:
|
||||
indent_depth = item.tree_depth - 1
|
||||
|
||||
for i in range(indent_depth):
|
||||
row.label(text="", icon="BLANK1")
|
||||
row.label(text="", icon="FILE_HIDDEN")
|
||||
|
||||
if item.document_type == "PROJECT":
|
||||
row.label(text="", icon="OUTLINER_COLLECTION")
|
||||
row.label(text=item.name)
|
||||
return
|
||||
|
||||
if item.document_type == "INFORMATION" and item.has_children:
|
||||
op = row.operator(
|
||||
"bim.toggle_document", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False
|
||||
)
|
||||
op.document = item.ifc_definition_id
|
||||
op.option = "Collapse" if item.is_expanded else "Expand"
|
||||
elif item.document_type == "INFORMATION":
|
||||
row.label(text="", icon="BLANK1")
|
||||
|
||||
if item.document_type == "INFORMATION":
|
||||
row.label(text="", icon="FILE")
|
||||
text = " - ".join([x for x in [item.location, item.description, item.name] if x])
|
||||
else:
|
||||
row.label(text="", icon="FILE_HIDDEN")
|
||||
text = " - ".join([x for x in [item.location, item.description] if x])
|
||||
split1 = row.split(factor=0.1)
|
||||
# split1.label(text=item.identification)
|
||||
split1.prop(item, "identification", text="", emboss=False)
|
||||
split2 = split1.split(factor=0.9)
|
||||
split2.prop(item, "name", text="", emboss=False)
|
||||
split2 = split1.split(factor=0.8)
|
||||
split2.label(text=text)
|
||||
|
||||
if item.location:
|
||||
uri = ObjectDocumentData.convert_to_file_uri(item.location)
|
||||
if item.location.lower().endswith(".ifc"):
|
||||
row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = uri
|
||||
row.operator("bim.open_uri", icon="URL", text="").uri = uri
|
||||
|
||||
|
||||
class BIM_UL_document_objects(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.prop(item, "name", text="", emboss=False, icon="OBJECT_DATA")
|
||||
row.operator("bim.select_object", text="", icon="RESTRICT_SELECT_OFF").obj_name = item.name
|
||||
|
||||
props = tool.Document.get_document_props()
|
||||
if props.active_document:
|
||||
document = props.active_document
|
||||
|
||||
op = row.operator("bim.unassign_document", text="", icon="X")
|
||||
op.document = document.ifc_definition_id
|
||||
op.obj = item.name
|
||||
|
||||
|
||||
def add_object_documents_context_menu(self, context):
|
||||
if not context.active_object:
|
||||
return
|
||||
|
||||
if not tool.Blender.get_ifc_definition_id(context.active_object):
|
||||
return
|
||||
|
||||
self.layout.separator()
|
||||
self.layout.menu("BIM_MT_object_documents_context_menu", icon="FILE")
|
||||
|
||||
|
||||
class BIM_MT_object_documents_context_menu(bpy.types.Menu):
|
||||
bl_idname = "BIM_MT_object_documents_context_menu"
|
||||
bl_label = "Documents"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
if not context.selected_objects:
|
||||
layout.label(text="No documents", icon="INFO")
|
||||
return
|
||||
|
||||
if len(context.selected_objects) > 1:
|
||||
layout.label(text="Select a single object to see its referenced documents", icon="INFO")
|
||||
return
|
||||
|
||||
obj = context.active_object
|
||||
if not obj or not tool.Blender.get_ifc_definition_id(obj):
|
||||
layout.label(text="No documents", icon="INFO")
|
||||
return
|
||||
|
||||
if not ObjectDocumentData.is_loaded:
|
||||
ObjectDocumentData.load()
|
||||
|
||||
if not ObjectDocumentData.data["documents"]:
|
||||
layout.label(text="No Documents", icon="FILE")
|
||||
else:
|
||||
for document in ObjectDocumentData.data["documents"]:
|
||||
row = layout.row(align=True)
|
||||
|
||||
with_ifc_icon = document["location"] and document["location"].lower().endswith(".ifc")
|
||||
with_url_icon = bool(document["location"])
|
||||
|
||||
if with_ifc_icon:
|
||||
row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = document["location"]
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
|
||||
if with_url_icon:
|
||||
row.operator("bim.open_uri", icon="URL", text="").uri = document["location"]
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
|
||||
doc_entity = None
|
||||
if "id" in document:
|
||||
doc_entity = tool.Ifc.get().by_id(document["id"])
|
||||
|
||||
if doc_entity and doc_entity.is_a("IfcDocumentReference"):
|
||||
display_text = document.get("description") or ""
|
||||
else:
|
||||
display_text = document.get("name") or ""
|
||||
|
||||
row.label(text=f"{document['identification'] or ''}: {display_text}")
|
||||
|
||||
@@ -158,6 +158,7 @@ def menu_func(self, context):
|
||||
if element and element.is_a("IfcAnnotation") and element.ObjectType in ["SECTION", "ELEVATION"]:
|
||||
self.layout.operator("bim.activate_drawing_by_annotation", text="Go to Drawing")
|
||||
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
|
||||
@@ -170,7 +171,7 @@ def register():
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
||||
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
@@ -313,15 +313,13 @@ def format_distance(
|
||||
if not feet and not add_inches:
|
||||
tx_dist += str(feet) + "'"
|
||||
|
||||
# Add "0' - " when we have inches but no feet
|
||||
# But only add " - " separator if we actually have inches to show
|
||||
if not feet and add_inches:
|
||||
tx_dist += "0' - "
|
||||
if value < 0:
|
||||
tx_dist += "-0' - "
|
||||
else:
|
||||
tx_dist += "0' - "
|
||||
elif feet and add_inches:
|
||||
tx_dist += " - "
|
||||
|
||||
if not feet and value < 0:
|
||||
tx_dist += "-"
|
||||
if add_inches:
|
||||
if feet == 0 and inches == 0 and not frac:
|
||||
# Special case: exactly zero, show "0"
|
||||
|
||||
@@ -353,20 +353,20 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
# Clear any local camera setup and force viewport to use scene camera
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
if area.type == "VIEW_3D":
|
||||
for space in area.spaces:
|
||||
if space.type == 'VIEW_3D':
|
||||
if space.type == "VIEW_3D":
|
||||
# Clear local camera to ensure we use scene.camera
|
||||
space.use_local_camera = False
|
||||
space.camera = context.scene.camera
|
||||
space.region_3d.view_perspective = 'CAMERA'
|
||||
space.region_3d.view_perspective = "CAMERA"
|
||||
print(f"Set viewport camera to: {context.scene.camera.name}")
|
||||
break
|
||||
|
||||
|
||||
# Force complete scene update
|
||||
context.view_layer.update()
|
||||
context.evaluated_depsgraph_get()
|
||||
|
||||
|
||||
underlay_svg = self.generate_underlay(context)
|
||||
|
||||
with profile("Generate linework"):
|
||||
@@ -3078,9 +3078,9 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||
filename_ext = ".svg"
|
||||
|
||||
|
||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)
|
||||
directory: bpy.props.StringProperty(subtype='DIR_PATH')
|
||||
directory: bpy.props.StringProperty(subtype="DIR_PATH")
|
||||
|
||||
def _execute(self, context):
|
||||
# Handle both single and multiple file selection
|
||||
@@ -3355,14 +3355,14 @@ class EnableEditingText(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for i, literal_backup in enumerate(literals_backup):
|
||||
if i < len(props.literals):
|
||||
literal_props = props.literals[i]
|
||||
|
||||
|
||||
if assigned_product_obj:
|
||||
literal_props.product_used = assigned_product_obj
|
||||
elif "product_used" in literal_backup and literal_backup["product_used"]:
|
||||
product_name = literal_backup["product_used"]
|
||||
if product_name in bpy.data.objects:
|
||||
literal_props.product_used = bpy.data.objects[product_name]
|
||||
|
||||
|
||||
literal_props.element_value_rows.clear()
|
||||
if "element_value_rows" in literal_backup:
|
||||
for row_data in literal_backup["element_value_rows"]:
|
||||
@@ -4251,63 +4251,62 @@ class ActivateDrawingByAnnotation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Activate Drawing"
|
||||
bl_description = "Activate the drawing corresponding to the selected annotation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Check if an annotation object is selected
|
||||
if not context.selected_objects:
|
||||
cls.poll_message_set("No object selected")
|
||||
return False
|
||||
|
||||
|
||||
active_obj = context.active_object
|
||||
if not active_obj:
|
||||
cls.poll_message_set("No active object")
|
||||
return False
|
||||
|
||||
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if not element:
|
||||
cls.poll_message_set("Selected object is not an IFC element")
|
||||
return False
|
||||
|
||||
|
||||
# Check if it's an IfcAnnotation with ObjectType = "SECTION" or "ELEVATION"
|
||||
if not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "ELEVATION"]:
|
||||
cls.poll_message_set("Selected object is not a drawing annotation")
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
active_obj = context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
|
||||
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "ELEVATION"]:
|
||||
self.report({"ERROR"}, "Selected object is not a drawing annotation")
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
# Find the drawing/camera element that this annotation references
|
||||
drawing_element = self.find_drawing_from_annotation(element)
|
||||
|
||||
|
||||
if not drawing_element:
|
||||
self.report({"ERROR"}, "Could not find drawing element for this annotation")
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
# Use the existing ActivateDrawing operator with the drawing element's ID
|
||||
bpy.ops.bim.activate_drawing(drawing=drawing_element.id())
|
||||
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def find_drawing_from_annotation(self, annotation_element):
|
||||
"""Find the drawing/camera element that this annotation references."""
|
||||
ifc = tool.Ifc.get()
|
||||
|
||||
|
||||
# Check IfcRelAssignsToProduct relationships
|
||||
for rel in ifc.get_inverse(annotation_element):
|
||||
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct:
|
||||
if rel.RelatingProduct.is_a("IfcAnnotation"):
|
||||
# Found the drawing element!
|
||||
return rel.RelatingProduct
|
||||
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -5062,7 +5061,7 @@ class AddElementValueRow(bpy.types.Operator):
|
||||
new_row.category = literal_props.category_for_adding
|
||||
new_row.element_key = ""
|
||||
new_row.formatted_value = ""
|
||||
|
||||
|
||||
if len(literal_props.element_value_rows) == 1:
|
||||
new_row.separator = ""
|
||||
else:
|
||||
@@ -5106,10 +5105,10 @@ class ElementValueSuggestionsPopup(bpy.types.Operator):
|
||||
row_index: bpy.props.IntProperty()
|
||||
category: bpy.props.StringProperty()
|
||||
search_query: bpy.props.StringProperty(name="Search", description="Search for element values")
|
||||
|
||||
|
||||
collection_keys: bpy.props.CollectionProperty(type=StrProperty)
|
||||
collection_descriptions: bpy.props.CollectionProperty(type=StrProperty)
|
||||
|
||||
|
||||
selected_key: bpy.props.StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -5157,13 +5156,13 @@ class ElementValueSuggestionsPopup(bpy.types.Operator):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
|
||||
layout.prop_search(self, "selected_key", self, "collection_descriptions", text="Value")
|
||||
|
||||
def execute(self, context):
|
||||
if not self.selected_key:
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -5177,7 +5176,7 @@ class ElementValueSuggestionsPopup(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
value_row = literal_props.element_value_rows[self.row_index]
|
||||
|
||||
|
||||
for idx, desc_item in enumerate(self.collection_descriptions):
|
||||
if desc_item.name == self.selected_key:
|
||||
actual_key = self.collection_keys[idx].name
|
||||
@@ -5260,10 +5259,7 @@ class FormatElementValueRow(bpy.types.Operator):
|
||||
|
||||
custom_expression: bpy.props.StringProperty(
|
||||
name="Custom Expression",
|
||||
description=(
|
||||
"Custom expression using functions\n"
|
||||
"Use {{value}} as placeholder for the current row's value."
|
||||
),
|
||||
description=("Custom expression using functions\n" "Use {{value}} as placeholder for the current row's value."),
|
||||
default='concat({{value}}, " - additional text")',
|
||||
)
|
||||
|
||||
@@ -5288,51 +5284,51 @@ class FormatElementValueRow(bpy.types.Operator):
|
||||
def _load_formatting_from_row(self, row):
|
||||
"""Parse the formatted_value to load existing formatting settings"""
|
||||
import re
|
||||
|
||||
|
||||
formatted_value = row.formatted_value
|
||||
|
||||
|
||||
if not formatted_value or formatted_value == f"{{{{{row.element_key}}}}}":
|
||||
self.formatting_type = "NONE"
|
||||
return
|
||||
|
||||
|
||||
if formatted_value.startswith("``") and formatted_value.endswith("``"):
|
||||
expression = formatted_value[2:-2].strip()
|
||||
else:
|
||||
self.formatting_type = "NONE"
|
||||
return
|
||||
|
||||
|
||||
if match := re.match(r"upper\(\{\{[^}]+\}\}\)", expression):
|
||||
self.formatting_type = "UPPER"
|
||||
|
||||
|
||||
elif match := re.match(r"lower\(\{\{[^}]+\}\}\)", expression):
|
||||
self.formatting_type = "LOWER"
|
||||
|
||||
|
||||
elif match := re.match(r"title\(\{\{[^}]+\}\}\)", expression):
|
||||
self.formatting_type = "TITLE"
|
||||
|
||||
|
||||
elif match := re.match(r"int\(\{\{[^}]+\}\}\)", expression):
|
||||
self.formatting_type = "INT"
|
||||
|
||||
|
||||
elif match := re.match(r"round\(\{\{[^}]+\}\},\s*([^)]+)\)", expression):
|
||||
self.formatting_type = "ROUND"
|
||||
self.round_precision = match.group(1).strip()
|
||||
|
||||
|
||||
elif match := re.match(r"number\(\{\{[^}]+\}\},\s*([^,]+),\s*([^)]+)\)", expression):
|
||||
self.formatting_type = "NUMBER"
|
||||
self.decimal_separator = match.group(1).strip()
|
||||
self.thousands_separator = match.group(2).strip()
|
||||
|
||||
|
||||
elif match := re.match(r"metric_length\(\{\{[^}]+\}\},\s*([^,]+),\s*([^)]+)\)", expression):
|
||||
self.formatting_type = "METRIC_LENGTH"
|
||||
self.metric_precision = match.group(1).strip()
|
||||
self.metric_decimals = int(match.group(2).strip())
|
||||
|
||||
|
||||
elif match := re.match(r'imperial_length\(\{\{[^}]+\}\},\s*(\d+),\s*"([^"]+)",\s*"([^"]+)"\)', expression):
|
||||
self.formatting_type = "IMPERIAL_LENGTH"
|
||||
self.imperial_precision = int(match.group(1).strip())
|
||||
self.imperial_input_unit = match.group(2).strip()
|
||||
self.imperial_output_unit = match.group(3).strip()
|
||||
|
||||
|
||||
else:
|
||||
self.formatting_type = "CUSTOM"
|
||||
self.custom_expression = expression
|
||||
@@ -5450,9 +5446,9 @@ class ApplyElementValueRowsToLiteral(bpy.types.Operator):
|
||||
default_format = f"{{{{{row.element_key}}}}}"
|
||||
row.formatted_value = default_format
|
||||
value_part = default_format
|
||||
|
||||
|
||||
parts.append(row.separator + value_part)
|
||||
|
||||
|
||||
concatenated_value = "".join(parts)
|
||||
|
||||
for attr in literal_props.attributes:
|
||||
@@ -5469,12 +5465,12 @@ class ApplyElementValueRowsToLiteral(bpy.types.Operator):
|
||||
This preserves formatting functions like upper(), round(), etc.
|
||||
"""
|
||||
import re
|
||||
|
||||
pattern = r'\{\{[^}]+\}\}'
|
||||
|
||||
|
||||
pattern = r"\{\{[^}]+\}\}"
|
||||
|
||||
new_base_value = f"{{{{{new_element_key}}}}}"
|
||||
updated_value = re.sub(pattern, new_base_value, old_formatted_value)
|
||||
|
||||
|
||||
return updated_value
|
||||
|
||||
|
||||
|
||||
@@ -714,7 +714,9 @@ class ElementValueRow(PropertyGroup):
|
||||
)
|
||||
|
||||
element_key: StringProperty(
|
||||
name="Element Key", description="The element value key (e.g., 'id', 'Name', 'Pset_WallCommon.Reference')", default=""
|
||||
name="Element Key",
|
||||
description="The element value key (e.g., 'id', 'Name', 'Pset_WallCommon.Reference')",
|
||||
default="",
|
||||
)
|
||||
|
||||
formatted_value: StringProperty(
|
||||
@@ -756,33 +758,33 @@ def get_category_items_with_counts(self, context):
|
||||
("Coordinates", "Coordinates", "Coordinate information", "EMPTY_ARROWS"),
|
||||
("Custom String", "Custom String", "Add custom text (no element key)", "SMALL_CAPS"),
|
||||
]
|
||||
|
||||
|
||||
obj = context.active_object
|
||||
|
||||
|
||||
if obj and tool.Ifc.get_entity(obj):
|
||||
try:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
text_element = element
|
||||
|
||||
if hasattr(self, 'product_used'):
|
||||
|
||||
if hasattr(self, "product_used"):
|
||||
if self.product_used:
|
||||
element = tool.Ifc.get_entity(self.product_used)
|
||||
else:
|
||||
assigned = tool.Drawing.get_assigned_product(text_element)
|
||||
if assigned:
|
||||
element = assigned
|
||||
|
||||
|
||||
available_keys = ElementValuesData.get_available_element_value_keys(element)
|
||||
items = []
|
||||
for i, (identifier, base_name, description, icon) in enumerate(category_metadata):
|
||||
count = len(available_keys.get(identifier, []))
|
||||
display_name = f"{base_name} ({count})" if count > 0 else base_name
|
||||
items.append((identifier, display_name, description, icon, i))
|
||||
|
||||
|
||||
return items
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
return [(id, name, desc, icon, i) for i, (id, name, desc, icon) in enumerate(category_metadata)]
|
||||
|
||||
|
||||
@@ -843,16 +845,16 @@ class LiteralProps(PropertyGroup):
|
||||
)
|
||||
|
||||
element_value_rows: CollectionProperty(
|
||||
name="Element Value Rows",
|
||||
name="Element Value Rows",
|
||||
type=ElementValueRow,
|
||||
description="Collection of element value rows for building the literal value"
|
||||
description="Collection of element value rows for building the literal value",
|
||||
)
|
||||
|
||||
category_for_adding: EnumProperty(
|
||||
name="Category for Adding",
|
||||
items=get_category_items_with_counts,
|
||||
default=0,
|
||||
description="Category to use when adding a new element value row"
|
||||
description="Category to use when adding a new element value row",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -115,9 +115,13 @@ def parse_markdown_it(text: str) -> list[dict[str, Union[str, None]]]:
|
||||
if tokens[j].type == "inline":
|
||||
for child in tokens[j].children or []:
|
||||
if child.type == "softbreak":
|
||||
segments.append({"text": None, "url": None, "break": True, "bold": False, "italic": False})
|
||||
segments.append(
|
||||
{"text": None, "url": None, "break": True, "bold": False, "italic": False}
|
||||
)
|
||||
elif child.type == "html_inline" and child.content.strip().lower() == "<br>":
|
||||
segments.append({"text": None, "url": None, "break": True, "bold": False, "italic": False})
|
||||
segments.append(
|
||||
{"text": None, "url": None, "break": True, "bold": False, "italic": False}
|
||||
)
|
||||
elif child.type == "strong_open":
|
||||
bold = True
|
||||
elif child.type == "strong_close":
|
||||
@@ -133,11 +137,27 @@ def parse_markdown_it(text: str) -> list[dict[str, Union[str, None]]]:
|
||||
elif child.type == "link_close" and link_opening:
|
||||
url = link_opening.attrGet("href")
|
||||
if url and link_text:
|
||||
segments.append({"text": link_text, "url": url, "break": False, "bold": bold, "italic": italic})
|
||||
segments.append(
|
||||
{
|
||||
"text": link_text,
|
||||
"url": url,
|
||||
"break": False,
|
||||
"bold": bold,
|
||||
"italic": italic,
|
||||
}
|
||||
)
|
||||
link_opening = None
|
||||
link_text = None
|
||||
elif child.type == "text" and not link_opening:
|
||||
segments.append({"text": child.content, "url": None, "break": False, "bold": bold, "italic": italic})
|
||||
segments.append(
|
||||
{
|
||||
"text": child.content,
|
||||
"url": None,
|
||||
"break": False,
|
||||
"bold": bold,
|
||||
"italic": italic,
|
||||
}
|
||||
)
|
||||
j += 1
|
||||
i = j
|
||||
else:
|
||||
@@ -168,7 +188,9 @@ def parse_markdown_it(text: str) -> list[dict[str, Union[str, None]]]:
|
||||
link_opening = None
|
||||
link_text = None
|
||||
elif child.type == "text" and not link_opening:
|
||||
segments.append({"text": child.content, "url": None, "break": False, "bold": bold, "italic": italic})
|
||||
segments.append(
|
||||
{"text": child.content, "url": None, "break": False, "bold": bold, "italic": italic}
|
||||
)
|
||||
i += 1
|
||||
segments = [seg for seg in segments if seg.get("text") is not None or seg.get("break", False)]
|
||||
if not segments:
|
||||
@@ -257,14 +279,16 @@ class SvgWriter:
|
||||
self.height = self.raw_height * self.svg_scale
|
||||
|
||||
def add_stylesheet(self):
|
||||
path = self.resource_paths["Stylesheet"]
|
||||
if not path:
|
||||
paths = self.resource_paths["Stylesheet"]
|
||||
if not paths:
|
||||
return
|
||||
if not os.path.exists(path):
|
||||
print(f"WARNING. Couldn't find stylesheet for the drawing by the path: {path}")
|
||||
return
|
||||
with open(path, "r") as stylesheet:
|
||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||
path_list = [p.strip() for p in paths.split(",")]
|
||||
for path in path_list:
|
||||
if not os.path.exists(path):
|
||||
print(f"WARNING. Couldn't find stylesheet for the drawing by the path: {path}")
|
||||
continue
|
||||
with open(path, "r") as stylesheet:
|
||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||
|
||||
def add_markers(self):
|
||||
path = self.resource_paths["Markers"]
|
||||
|
||||
@@ -476,7 +476,6 @@ class BIM_PT_sheets(Panel):
|
||||
|
||||
op = row3.operator("bim.activate_drawing_from_sheet", icon="OUTLINER_OB_CAMERA", text="")
|
||||
|
||||
|
||||
if active_sheet.reference_type == "DRAWING":
|
||||
drawingnamesvg = active_sheet.name
|
||||
drawingname = drawingnamesvg.split(".svg")[0]
|
||||
@@ -679,14 +678,14 @@ class BIM_PT_text(Panel):
|
||||
|
||||
if len(literal_props.attributes) > 0 and i < len(props.literal_apply_settings):
|
||||
row = box.row(align=True)
|
||||
row.prop(literal_props.attributes[0], "string_value", text="Literal")
|
||||
|
||||
bonsai.bim.helper.draw_attribute(literal_props.attributes[0], row, enable_search=True)
|
||||
|
||||
expand_icon = "DOWNARROW_HLT" if getattr(literal_props, "show_element_values", False) else "RIGHTARROW"
|
||||
op = row.operator("bim.toggle_element_values_panel", icon=expand_icon, text="")
|
||||
op.literal_prop_id = i
|
||||
|
||||
|
||||
row.prop(props.literal_apply_settings[i], "apply_text_to_all", text="", icon="COPYDOWN")
|
||||
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assigned_element = tool.Drawing.get_assigned_product(element) or element
|
||||
resolved_value = tool.Drawing.replace_text_literal_variables(
|
||||
@@ -711,8 +710,10 @@ class BIM_PT_text(Panel):
|
||||
element_values_row.prop(literal_props, "product_used", text="", icon="EYEDROPPER")
|
||||
|
||||
current_product = get_current_product_for_element_values(obj, literal_props)
|
||||
|
||||
product_name = current_product.name if (current_product and hasattr(current_product, "name")) else "Unknown"
|
||||
|
||||
product_name = (
|
||||
current_product.name if (current_product and hasattr(current_product, "name")) else "Unknown"
|
||||
)
|
||||
source_row = values_box.row()
|
||||
source_row.label(text=f"Source: {product_name}", icon="OBJECT_DATA")
|
||||
|
||||
@@ -720,29 +721,29 @@ class BIM_PT_text(Panel):
|
||||
if element:
|
||||
add_row = values_box.row(align=True)
|
||||
add_row.prop(literal_props, "category_for_adding", text="")
|
||||
|
||||
|
||||
op = add_row.operator("bim.add_element_value_row", text="Add Element", icon="ADD")
|
||||
op.literal_prop_id = i
|
||||
|
||||
if len(literal_props.element_value_rows) > 0:
|
||||
for row_idx, value_row in enumerate(literal_props.element_value_rows):
|
||||
row = values_box.row(align=True)
|
||||
|
||||
|
||||
is_custom_string = value_row.category == "Custom String"
|
||||
|
||||
|
||||
if is_custom_string:
|
||||
category_icon = get_category_icon(value_row.category)
|
||||
row.prop(value_row, "element_key", text="", icon=category_icon)
|
||||
else:
|
||||
split = row.split(factor=0.25, align=True)
|
||||
|
||||
|
||||
sep_col = split.row(align=True)
|
||||
sep_col.prop(value_row, "separator", text="")
|
||||
|
||||
|
||||
key_col = split.row(align=True)
|
||||
category_icon = get_category_icon(value_row.category)
|
||||
key_col.prop(value_row, "element_key", text="", icon=category_icon)
|
||||
|
||||
|
||||
op = row.operator("bim.element_value_suggestions_popup", text="", icon="VIEWZOOM")
|
||||
op.literal_prop_id = i
|
||||
op.row_index = row_idx
|
||||
@@ -758,7 +759,9 @@ class BIM_PT_text(Panel):
|
||||
|
||||
apply_row = values_box.row()
|
||||
apply_row.scale_y = 1.2
|
||||
op = apply_row.operator("bim.apply_element_value_rows_to_literal", text="Apply to Literal", icon="CHECKMARK")
|
||||
op = apply_row.operator(
|
||||
"bim.apply_element_value_rows_to_literal", text="Apply to Literal", icon="CHECKMARK"
|
||||
)
|
||||
op.literal_prop_id = i
|
||||
else:
|
||||
error_row = values_box.row()
|
||||
|
||||
@@ -1205,13 +1205,84 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
for obj in objects_to_remove:
|
||||
tool.Blender.deselect_object(obj)
|
||||
|
||||
# Expand selection to include all parts of selected aggregates
|
||||
objects_to_duplicate = set(context.selected_objects) - objects_to_remove
|
||||
expanded_objects = set(objects_to_duplicate)
|
||||
|
||||
for obj in objects_to_duplicate:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.is_a("IfcElementAssembly"):
|
||||
parts = tool.Aggregate.get_parts_recursively(element)
|
||||
for part in parts:
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
if part_obj:
|
||||
expanded_objects.add(part_obj)
|
||||
|
||||
# Store parent aggregate relationships
|
||||
parent_aggregates = {}
|
||||
|
||||
for obj in expanded_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.is_a("IfcElementAssembly"):
|
||||
parent_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if parent_aggregate:
|
||||
parent_aggregates[element] = parent_aggregate
|
||||
|
||||
old_to_new, new_active_obj = tool.Geometry.duplicate_ifc_objects(
|
||||
set(context.selected_objects) - objects_to_remove,
|
||||
expanded_objects,
|
||||
linked=linked,
|
||||
active_object=context.active_object,
|
||||
)
|
||||
|
||||
# Restore parent aggregate relationships, but only for parents that were NOT duplicated
|
||||
for old_elem, new_elems in old_to_new.items():
|
||||
if old_elem in parent_aggregates:
|
||||
old_parent = parent_aggregates[old_elem]
|
||||
|
||||
# Check if the parent was also duplicated
|
||||
if old_parent in old_to_new:
|
||||
# The duplication already created the correct nested relationship
|
||||
continue
|
||||
|
||||
# Parent was NOT duplicated, so we need to assign to the original parent
|
||||
for new_elem in new_elems:
|
||||
new_obj = tool.Ifc.get_object(new_elem)
|
||||
parent_obj = tool.Ifc.get_object(old_parent)
|
||||
if new_obj and parent_obj:
|
||||
bonsai.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=parent_obj,
|
||||
related_obj=new_obj,
|
||||
)
|
||||
|
||||
# Select all duplicated objects and their parts
|
||||
all_objects_to_select = set()
|
||||
for old_elem, new_elems in old_to_new.items():
|
||||
for new_elem in new_elems:
|
||||
new_obj = tool.Ifc.get_object(new_elem)
|
||||
if new_obj:
|
||||
all_objects_to_select.add(new_obj)
|
||||
|
||||
# If it's an aggregate, also select all its parts
|
||||
if new_elem.is_a("IfcElementAssembly"):
|
||||
parts = tool.Aggregate.get_parts_recursively(new_elem)
|
||||
for part in parts:
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
if part_obj:
|
||||
all_objects_to_select.add(part_obj)
|
||||
|
||||
# Deselect everything first
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
# Select all the duplicated objects
|
||||
for obj in all_objects_to_select:
|
||||
obj.select_set(True)
|
||||
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
|
||||
return old_to_new
|
||||
|
||||
|
||||
@@ -1491,6 +1562,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
old_to_new = {}
|
||||
original_data: dict[int, dict[int, dict[str, Any]]] = {}
|
||||
|
||||
# Define all nested functions FIRST
|
||||
def delete_objects(element: ifcopenshell.entity_instance) -> None:
|
||||
"""Remove IfcElementAssembly and it's parts."""
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
@@ -1542,7 +1614,10 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if self.group_name in r.RelatingGroup.Name
|
||||
).id()
|
||||
original_data[group] = {}
|
||||
|
||||
# Initialize if not exists
|
||||
if group not in original_data:
|
||||
original_data[group] = {}
|
||||
|
||||
pset: dict[str, Any] = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index: int = pset["Index"]
|
||||
@@ -1559,8 +1634,13 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if parts:
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
# TODO: unused expression.
|
||||
original_data | get_original_data(part)
|
||||
# Recursively collect data from nested assemblies
|
||||
nested_data = get_original_data(part)
|
||||
# Merge nested data into original_data
|
||||
for nested_group_id, nested_group_data in nested_data.items():
|
||||
if nested_group_id not in original_data:
|
||||
original_data[nested_group_id] = {}
|
||||
original_data[nested_group_id].update(nested_group_data)
|
||||
else:
|
||||
try:
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
@@ -1584,50 +1664,123 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
aggregate = element
|
||||
|
||||
group = next(
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if self.group_name in r.RelatingGroup.Name
|
||||
).id()
|
||||
if not group:
|
||||
# Get the new group
|
||||
new_group_entity = next(
|
||||
(
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if self.group_name in r.RelatingGroup.Name
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not new_group_entity:
|
||||
return
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
if not pset:
|
||||
return
|
||||
|
||||
index = pset["Index"]
|
||||
|
||||
# Find the matching old group by looking for the same aggregate name
|
||||
matching_group_id = None
|
||||
if index == 0:
|
||||
obj.name = pset["Name"] + "_" + str(original_data[group][index]["Aggregate_Index"])
|
||||
# This is a root assembly - find by Name
|
||||
aggregate_name = pset.get("Name")
|
||||
for group_id, group_data in original_data.items():
|
||||
if 0 in group_data and group_data[0].get("Name") == aggregate_name:
|
||||
matching_group_id = group_id
|
||||
break
|
||||
else:
|
||||
# This is a part - find the group that has this index
|
||||
for group_id, group_data in original_data.items():
|
||||
if index in group_data:
|
||||
matching_group_id = group_id
|
||||
break
|
||||
|
||||
if matching_group_id is None:
|
||||
return
|
||||
|
||||
if index == 0:
|
||||
obj.name = pset["Name"] + "_" + str(original_data[matching_group_id][index]["Aggregate_Index"])
|
||||
ifc_file = tool.Ifc.get()
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
ifc_file.by_id(pset["id"]),
|
||||
properties={"Aggregate_Index": int(original_data[group][index]["Aggregate_Index"])},
|
||||
properties={"Aggregate_Index": int(original_data[matching_group_id][index]["Aggregate_Index"])},
|
||||
)
|
||||
bonsai.core.spatial.assign_container(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Spatial,
|
||||
container=original_data[group][index]["Container"],
|
||||
element_obj=obj,
|
||||
)
|
||||
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
|
||||
tool.Collector.assign(tool.Ifc.get_object(part))
|
||||
assignments = original_data[group][index]["Assignment"]
|
||||
if assignments:
|
||||
assign_to_annotations(obj, assignments)
|
||||
|
||||
# Only assign container if element is not already aggregated under another element
|
||||
# Aggregated elements should not be in the spatial structure
|
||||
if not ifcopenshell.util.element.get_aggregate(element):
|
||||
container = original_data[matching_group_id][index]["Container"]
|
||||
bonsai.core.spatial.assign_container(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Spatial,
|
||||
container=container,
|
||||
element_obj=obj,
|
||||
)
|
||||
|
||||
# Get the container's collection for moving parts in the outliner
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
container_collection = container_obj.BIMObjectProperties.collection if container_obj else None
|
||||
|
||||
# Move all parts to the container's collection in the outliner
|
||||
if container_collection:
|
||||
for part in ifcopenshell.util.element.get_parts(element):
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
if part_obj:
|
||||
# Remove from all previous collections
|
||||
for col in part_obj.users_collection[:]:
|
||||
col.objects.unlink(part_obj)
|
||||
|
||||
# Link to container collection
|
||||
if part_obj.name not in container_collection.objects:
|
||||
container_collection.objects.link(part_obj)
|
||||
|
||||
# Recursively handle nested parts
|
||||
for nested_part in ifcopenshell.util.element.get_parts(part):
|
||||
nested_part_obj = tool.Ifc.get_object(nested_part)
|
||||
if nested_part_obj:
|
||||
for col in nested_part_obj.users_collection[:]:
|
||||
col.objects.unlink(nested_part_obj)
|
||||
if nested_part_obj.name not in container_collection.objects:
|
||||
container_collection.objects.link(nested_part_obj)
|
||||
else:
|
||||
try:
|
||||
obj.name = original_data[group][index]["Name"]
|
||||
obj.name = original_data[matching_group_id][index]["Name"]
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
assignments = original_data[group][index]["Assignment"]
|
||||
assignments = original_data[matching_group_id][index]["Assignment"]
|
||||
except:
|
||||
assignments = []
|
||||
if assignments:
|
||||
assign_to_annotations(obj, assignments)
|
||||
|
||||
def get_original_matrix(
|
||||
element: ifcopenshell.entity_instance, base_instance: ifcopenshell.entity_instance
|
||||
) -> tuple[Matrix, tuple[Vector, Quaternion, Vector]]:
|
||||
selected_obj = tool.Ifc.get_object(base_instance)
|
||||
selected_matrix = selected_obj.matrix_world
|
||||
object_duplicate = tool.Ifc.get_object(element)
|
||||
duplicate_matrix = object_duplicate.matrix_world.decompose()
|
||||
|
||||
return selected_matrix, duplicate_matrix
|
||||
|
||||
def set_new_matrix(
|
||||
selected_matrix: Matrix, duplicate_matrix: tuple[Vector, Quaternion, Vector], old_to_new: dict
|
||||
) -> None:
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
|
||||
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
|
||||
new_obj_matrix = new_base_matrix @ matrix_diff
|
||||
new_obj.matrix_world = new_obj_matrix
|
||||
|
||||
def get_element_assembly(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if element.is_a("IfcElementAssembly"):
|
||||
return element
|
||||
@@ -1671,26 +1824,6 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
return list(set(linked_aggregate_groups)), selected_parents
|
||||
|
||||
def get_original_matrix(
|
||||
element: ifcopenshell.entity_instance, base_instance: ifcopenshell.entity_instance
|
||||
) -> tuple[Matrix, tuple[Vector, Quaternion, Vector]]:
|
||||
selected_obj = tool.Ifc.get_object(base_instance)
|
||||
selected_matrix = selected_obj.matrix_world
|
||||
object_duplicate = tool.Ifc.get_object(element)
|
||||
duplicate_matrix = object_duplicate.matrix_world.decompose()
|
||||
|
||||
return selected_matrix, duplicate_matrix
|
||||
|
||||
def set_new_matrix(
|
||||
selected_matrix: Matrix, duplicate_matrix: tuple[Vector, Quaternion, Vector], old_to_new: dict
|
||||
) -> None:
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
|
||||
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
|
||||
new_obj_matrix = new_base_matrix @ matrix_diff
|
||||
new_obj.matrix_world = new_obj_matrix
|
||||
|
||||
active_element = tool.Ifc.get_entity(context.active_object)
|
||||
if not active_element:
|
||||
self.report({"INFO"}, "Object has no Ifc metadata.")
|
||||
@@ -1727,6 +1860,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
base_pset = ifcopenshell.util.element.get_pset(base_instance, self.pset_name)
|
||||
base_obj = tool.Ifc.get_object(base_instance)
|
||||
base_obj.name = base_pset["Name"] + "_" + str(base_pset["Aggregate_Index"])
|
||||
|
||||
for element in instances_to_refresh:
|
||||
if element.GlobalId == base_instance.GlobalId:
|
||||
continue
|
||||
@@ -1735,7 +1869,12 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
|
||||
|
||||
original_data = get_original_data(element)
|
||||
# Merge data instead of overwriting
|
||||
element_original_data = get_original_data(element)
|
||||
for group_id, group_data in element_original_data.items():
|
||||
if group_id not in original_data:
|
||||
original_data[group_id] = {}
|
||||
original_data[group_id].update(group_data)
|
||||
|
||||
delete_objects(element)
|
||||
|
||||
@@ -1749,7 +1888,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
if element_aggregate and new[0].is_a("IfcElementAssembly"):
|
||||
if element_aggregate and new[0].is_a("IfcElementAssembly") and old == base_instance:
|
||||
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
|
||||
|
||||
if not new_aggregate:
|
||||
|
||||
@@ -53,12 +53,13 @@ class GeoreferenceDecorator:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None, should_scale=True):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
self.scale = props.visualization_scale
|
||||
content_pos = [v * self.scale for v in content_pos]
|
||||
if should_scale:
|
||||
scale = tool.Georeference.get_georeference_props().visualization_scale
|
||||
content_pos = [v * scale for v in content_pos]
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -141,12 +142,13 @@ class GeoreferenceDecorator:
|
||||
|
||||
if wcs["blender_location"].length < 1000:
|
||||
position = wcs["blender_location"].copy()
|
||||
position -= Vector((0, 0.1, 0))
|
||||
self.draw_text_at_position(context, text, position, should_scale=False)
|
||||
else:
|
||||
position = wcs["blender_location"].normalized() * 3
|
||||
text += "\n(Warning: Actual XYZ Not Shown)"
|
||||
position -= Vector((0, 0.1, 0))
|
||||
|
||||
self.draw_text_at_position(context, text, position)
|
||||
position -= Vector((0, 0.1, 0))
|
||||
self.draw_text_at_position(context, text, position)
|
||||
|
||||
if props.has_blender_offset:
|
||||
text = "IFC Local Origin"
|
||||
@@ -165,8 +167,10 @@ class GeoreferenceDecorator:
|
||||
self.draw_text_at_position(context, text, location)
|
||||
blf.disable(self.font_id, blf.SHADOW)
|
||||
|
||||
def draw_text_at_position(self, context, text, position):
|
||||
position = [v * self.scale for v in position]
|
||||
def draw_text_at_position(self, context, text, position, should_scale=True):
|
||||
if should_scale:
|
||||
scale = tool.Georeference.get_georeference_props().visualization_scale
|
||||
position = [v * scale for v in position]
|
||||
coords_2d = location_3d_to_region_2d(context.region, context.region_data, position)
|
||||
if not coords_2d:
|
||||
return
|
||||
@@ -309,8 +313,8 @@ class GeoreferenceDecorator:
|
||||
if wcs["blender_location"].length < 1000:
|
||||
verts = [Vector((0, 0, 0)), wcs["blender_location"]]
|
||||
edges = [[0, 1]]
|
||||
self.draw_batch("LINES", verts, decorator_color_special, edges)
|
||||
self.draw_batch("POINTS", verts[1:], decorator_color_special)
|
||||
self.draw_batch("LINES", verts, decorator_color_special, edges, should_scale=False)
|
||||
self.draw_batch("POINTS", verts[1:], decorator_color_special, should_scale=False)
|
||||
else:
|
||||
location = wcs["blender_location"].normalized()
|
||||
edges = [[0, 1]]
|
||||
@@ -332,7 +336,7 @@ class GeoreferenceDecorator:
|
||||
self.draw_batch("LINES", verts, decorator_color_special, edges)
|
||||
self.draw_dashed_line(location * 3, location * 6, decorator_color_error)
|
||||
|
||||
def draw_dashed_line(self, start, end, colour):
|
||||
def draw_dashed_line(self, start, end, colour, should_scale=True):
|
||||
direction = (end - start).normalized()
|
||||
distance = (end - start).length
|
||||
current_distance = Vector((0, 0, 0))
|
||||
@@ -347,7 +351,7 @@ class GeoreferenceDecorator:
|
||||
|
||||
edges = [[i, i + 1] for i in range(0, len(points), 2)]
|
||||
verts = points
|
||||
self.draw_batch("LINES", verts, colour, edges)
|
||||
self.draw_batch("LINES", verts, colour, edges, should_scale=should_scale)
|
||||
|
||||
def calculate_angles(self, context):
|
||||
self.pn_angle = 0.0
|
||||
|
||||
@@ -215,7 +215,7 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
||||
description="Affects the georeference decorator size",
|
||||
default=1,
|
||||
soft_min=0.1,
|
||||
soft_max=50,
|
||||
soft_max=100,
|
||||
)
|
||||
grid_north_angle: StringProperty(name="Grid North Angle", update=update_grid_north_angle)
|
||||
x_axis_abscissa: StringProperty(name="X Axis Abscissa", update=update_grid_north_vector)
|
||||
|
||||
@@ -176,6 +176,7 @@ class ObjectMaterialData:
|
||||
cls.data["active_material_constituents"] = cls.active_material_constituents()
|
||||
# after material_name and type_material
|
||||
cls.data["is_type_material_overridden"] = cls.is_type_material_overridden()
|
||||
cls.data["bbim_material_layer_pset"] = cls.bbim_material_layer_pset()
|
||||
|
||||
cls.is_loaded = True
|
||||
|
||||
@@ -426,3 +427,35 @@ class ObjectMaterialData:
|
||||
# so we check occurrence material explicitly
|
||||
occurrence_material = ifcopenshell.util.element.get_material(cls.element, should_inherit=False)
|
||||
return bool(occurrence_material)
|
||||
|
||||
@classmethod
|
||||
def bbim_material_layer_pset(cls) -> Union[dict[str, Any], None]:
|
||||
"""Load BBIM_MaterialLayer pset data for display in UI."""
|
||||
if not cls.element:
|
||||
return None
|
||||
|
||||
pset_data = ifcopenshell.util.element.get_pset(cls.element, "BBIM_MaterialLayer")
|
||||
if not pset_data or not pset_data.get("UseCustomOffset", False):
|
||||
return None
|
||||
|
||||
# Keep offset in SI units - format_distance will handle conversion
|
||||
custom_offset_si = pset_data.get("CustomOffset", 0.0)
|
||||
|
||||
# Get the appropriate reference based on usage type
|
||||
usage_type = tool.Model.get_usage_type(cls.element)
|
||||
custom_reference = None
|
||||
reference_label = None
|
||||
|
||||
if usage_type == "LAYER2":
|
||||
custom_reference = pset_data.get("CustomWallReference", "")
|
||||
reference_label = "Wall Reference"
|
||||
elif usage_type == "LAYER3":
|
||||
custom_reference = pset_data.get("CustomSlabReference", "")
|
||||
reference_label = "Slab Reference"
|
||||
|
||||
return {
|
||||
"use_custom_offset": pset_data.get("UseCustomOffset", False),
|
||||
"custom_offset": custom_offset_si, # Store in SI units
|
||||
"custom_reference": custom_reference,
|
||||
"reference_label": reference_label,
|
||||
}
|
||||
|
||||
@@ -509,6 +509,10 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
|
||||
bonsai.bim.helper.import_attributes(material[0], props.material_set_attributes)
|
||||
else:
|
||||
bonsai.bim.helper.import_attributes(material, props.material_set_attributes)
|
||||
|
||||
# Load custom offset from BBIM_MaterialLayer pset
|
||||
tool.Model.load_custom_offset_from_pset(element, obj)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def import_attributes_callback(
|
||||
@@ -622,12 +626,16 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet)
|
||||
|
||||
# Save custom offset to BBIM_MaterialLayer pset
|
||||
tool.Model.save_custom_offset_to_pset(obj_element, obj)
|
||||
|
||||
for layer_set in layer_sets_to_regenerate:
|
||||
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
|
||||
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
|
||||
|
||||
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
|
||||
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
|
||||
if "CardinalPoint" in attributes:
|
||||
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
|
||||
ifcopenshell.api.material.edit_profile_usage(
|
||||
self.file,
|
||||
usage=material_set_usage,
|
||||
|
||||
@@ -20,6 +20,8 @@ from __future__ import annotations
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import bpy
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.helper import draw_attributes
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
@@ -134,8 +136,6 @@ class BIM_PT_object_material(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Blender.is_tab(context, "GEOMETRY"):
|
||||
return False
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(obj)
|
||||
@@ -228,31 +228,47 @@ class BIM_PT_object_material(Panel):
|
||||
self.draw_read_only_set_ui()
|
||||
|
||||
def draw_editable_set_ui(self):
|
||||
bonsai.bim.helper.draw_attributes(self.props.material_set_attributes, self.layout)
|
||||
bonsai.bim.helper.draw_attributes(self.props.material_set_usage_attributes, self.layout)
|
||||
# Material Set Attributes Section
|
||||
row = self.layout.row(align=True)
|
||||
box = row.box()
|
||||
|
||||
bonsai.bim.helper.draw_attributes(self.props.material_set_attributes, box)
|
||||
bonsai.bim.helper.draw_attributes(self.props.material_set_usage_attributes, box)
|
||||
|
||||
# Custom Offset Section
|
||||
self.draw_custom_offset()
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile" and not self.mprops.profiles:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Profiles Available")
|
||||
row.operator("bim.add_profile_def", icon="ADD", text="")
|
||||
else:
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.separator()
|
||||
row = self.layout.row(align=True)
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile":
|
||||
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="")
|
||||
prop_with_search(row, self.props, "material", icon="MATERIAL", text="")
|
||||
op = row.operator(f"bim.add_{ObjectMaterialData.data['set_item_name']}", icon="ADD", text="")
|
||||
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
|
||||
|
||||
# Dynamic header based on material set type
|
||||
set_item_name = ObjectMaterialData.data["set_item_name"]
|
||||
header_map = {
|
||||
"layer": "Material Layers",
|
||||
"profile": "Material Profiles",
|
||||
"constituent": "Material Constituents",
|
||||
"list_item": "Material List Items",
|
||||
}
|
||||
header_text = header_map.get(set_item_name, "Material Items")
|
||||
self.layout.label(text=header_text)
|
||||
|
||||
total_items = len(ObjectMaterialData.data["set_items"])
|
||||
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
row = self.layout.row(align=True)
|
||||
box = row.box()
|
||||
|
||||
# Add Material Section (at the top of this box)
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile" and not self.mprops.profiles:
|
||||
box_row = box.row(align=True)
|
||||
box_row.label(text="No Profiles Available")
|
||||
box_row.operator("bim.add_profile_def", icon="ADD", text="")
|
||||
else:
|
||||
box_row = box.row(align=True)
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile":
|
||||
prop_with_search(box_row, self.mprops, "profiles", icon="ITALIC", text="")
|
||||
prop_with_search(box_row, self.props, "material", icon="MATERIAL", text="")
|
||||
op = box_row.operator(f"bim.add_{ObjectMaterialData.data['set_item_name']}", icon="ADD", text="")
|
||||
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
|
||||
|
||||
active_object = bpy.context.active_object
|
||||
self.layerset_bounds(box, active_object, location="Top_Exterior")
|
||||
self.layerset_bounds(box, active_object, location="Top_Interior")
|
||||
|
||||
if not ObjectMaterialData.data["set_items"]:
|
||||
row = box.row()
|
||||
@@ -269,7 +285,7 @@ class BIM_PT_object_material(Panel):
|
||||
else:
|
||||
self.draw_read_only_set_item_ui(box, set_item)
|
||||
|
||||
self.layerset_bounds(box, active_object, location="Bottom_Interior")
|
||||
self.layerset_bounds(box, active_object, location="Bottom_Exterior")
|
||||
|
||||
def draw_editable_set_item_profile_ui(self, box, set_item):
|
||||
# box = self.layout.box()
|
||||
@@ -335,29 +351,100 @@ class BIM_PT_object_material(Panel):
|
||||
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_index", set_item["index"])
|
||||
|
||||
def draw_read_only_set_ui(self):
|
||||
# Material Set Information Section
|
||||
row = self.layout.row(align=True)
|
||||
box = row.box()
|
||||
|
||||
if ObjectMaterialData.data["material_class"] != "IfcMaterialList":
|
||||
row = self.layout.row(align=True)
|
||||
box_row = box.row(align=True)
|
||||
set_name = ObjectMaterialData.data["set"]["name"]
|
||||
row.label(text="Name")
|
||||
row.label(text=set_name)
|
||||
box_row.label(text="Name")
|
||||
box_row.label(text=set_name)
|
||||
|
||||
if value := ObjectMaterialData.data["set"]["description"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Description")
|
||||
row.label(text=value)
|
||||
box_row = box.row(align=True)
|
||||
box_row.label(text="Description")
|
||||
box_row.label(text=value)
|
||||
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterialProfileSetUsage":
|
||||
if value := ObjectMaterialData.data["set_usage"].get("cardinal_point"):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Cardinal Point")
|
||||
row.label(text=value)
|
||||
box_row = box.row(align=True)
|
||||
box_row.label(text="Cardinal Point")
|
||||
box_row.label(text=value)
|
||||
|
||||
if ObjectMaterialData.data["total_thickness"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Total Thickness*")
|
||||
row.label(text=ObjectMaterialData.data["total_thickness"])
|
||||
box_row = box.row(align=True)
|
||||
box_row.label(text="Total Thickness*")
|
||||
box_row.label(text=ObjectMaterialData.data["total_thickness"])
|
||||
|
||||
box = self.layout.box()
|
||||
# Display OffsetFromReferenceLine for layer sets
|
||||
if "Layer" in ObjectMaterialData.data["material_class"]:
|
||||
obj = bpy.context.active_object
|
||||
if obj:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
offset_value = material.OffsetFromReferenceLine
|
||||
# Format the offset value
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
precision = None
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = prefs.doc.imperial_precision
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
|
||||
formatted_offset = format_distance(
|
||||
offset_value, precision=precision, suppress_zero_inches=True, in_unit_length=True
|
||||
)
|
||||
box_row = box.row(align=True)
|
||||
box_row.label(text="Offset From Reference Line")
|
||||
box_row.label(text=formatted_offset)
|
||||
|
||||
# BBIM_MaterialLayer Pset Section
|
||||
if pset_data := ObjectMaterialData.data.get("bbim_material_layer_pset"):
|
||||
self.layout.label(text="BBIM_MaterialLayer Pset")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
box = row.box()
|
||||
|
||||
# Custom Offset value - format using format_distance
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
precision = None
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = prefs.doc.imperial_precision
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
|
||||
formatted_custom_offset = format_distance(
|
||||
pset_data["custom_offset"], precision=precision, suppress_zero_inches=True, in_unit_length=True
|
||||
)
|
||||
box_row = box.row(align=True)
|
||||
box_row.label(text="Custom Offset")
|
||||
box_row.label(text=formatted_custom_offset)
|
||||
|
||||
# Reference (if exists)
|
||||
if pset_data["custom_reference"]:
|
||||
box_row = box.row(align=True)
|
||||
box_row.label(text=pset_data["reference_label"])
|
||||
box_row.label(text=pset_data["custom_reference"])
|
||||
|
||||
# Dynamic header based on material set type
|
||||
set_item_name = ObjectMaterialData.data.get("set_item_name")
|
||||
if set_item_name:
|
||||
header_map = {
|
||||
"layer": "Material Layers",
|
||||
"profile": "Material Profiles",
|
||||
"constituent": "Material Constituents",
|
||||
"list_item": "Material List Items",
|
||||
}
|
||||
header_text = header_map.get(set_item_name, "Material Items")
|
||||
else:
|
||||
header_text = "Materials"
|
||||
|
||||
self.layout.label(text=header_text)
|
||||
row = self.layout.row(align=True)
|
||||
box = row.box()
|
||||
active_object = bpy.context.active_object
|
||||
self.layerset_bounds(box, active_object, location="Top_Interior")
|
||||
|
||||
@@ -403,20 +490,27 @@ class BIM_PT_object_material(Panel):
|
||||
set_usage = ObjectMaterialData.data.get("set_usage", {})
|
||||
layer_set_direction = set_usage.get("layer_set_direction")
|
||||
if layer_set_direction:
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
row.prop(self.props, "use_custom_offset", text="Use Custom Offset")
|
||||
row = box.row(align=True)
|
||||
if layer_set_direction == "AXIS2":
|
||||
row.prop(self.props, "custom_wall_reference", text="Reference")
|
||||
row.enabled = self.props.use_custom_offset
|
||||
if layer_set_direction == "AXIS3":
|
||||
row.prop(self.props, "custom_slab_reference", text="Reference")
|
||||
row.enabled = self.props.use_custom_offset
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="BBIM_MaterialLayer Pset")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(self.props, "custom_offset", text="Custom Offset")
|
||||
row.enabled = self.props.use_custom_offset
|
||||
# Add indentation with a row that has a separator
|
||||
row = self.layout.row(align=True)
|
||||
# row.separator(factor=2.0) # Adjust factor for more/less indent
|
||||
|
||||
box = row.box()
|
||||
box_row = box.row(align=True)
|
||||
box_row.prop(self.props, "use_custom_offset", text="Use Custom Offset")
|
||||
box_row = box.row(align=True)
|
||||
if layer_set_direction == "AXIS2":
|
||||
box_row.prop(self.props, "custom_wall_reference", text="Reference")
|
||||
box_row.enabled = self.props.use_custom_offset
|
||||
if layer_set_direction == "AXIS3":
|
||||
box_row.prop(self.props, "custom_slab_reference", text="Reference")
|
||||
box_row.enabled = self.props.use_custom_offset
|
||||
|
||||
box_row = box.row(align=True)
|
||||
box_row.prop(self.props, "custom_offset", text="Custom Offset")
|
||||
box_row.enabled = self.props.use_custom_offset
|
||||
|
||||
|
||||
class BIM_UL_materials(UIList):
|
||||
|
||||
@@ -170,7 +170,7 @@ class FilledOpeningGenerator:
|
||||
reuse_mapped_representation = True
|
||||
else:
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
|
||||
if not reuse_mapped_representation:
|
||||
# Check for library template before generating from filling
|
||||
template_rep = self.get_opening_template_from_type(filling)
|
||||
@@ -191,25 +191,25 @@ class FilledOpeningGenerator:
|
||||
MappingSource=existing_mapping_source,
|
||||
MappingTarget=tool.Ifc.get().create_entity(
|
||||
"IfcCartesianTransformationOperator3D",
|
||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1., 0., 0.)),
|
||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 1., 0.)),
|
||||
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0., 0., 0.)),
|
||||
Scale=1.,
|
||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 0., 1.))
|
||||
)
|
||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
|
||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)),
|
||||
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
|
||||
Scale=1.0,
|
||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
|
||||
),
|
||||
)
|
||||
mapped_representation = tool.Ifc.get().create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
ContextOfItems=context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="MappedRepresentation",
|
||||
Items=[new_mapped_item]
|
||||
Items=[new_mapped_item],
|
||||
)
|
||||
else:
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
tool.Ifc.get(), representation=representation
|
||||
)
|
||||
|
||||
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
@@ -333,25 +333,25 @@ class FilledOpeningGenerator:
|
||||
MappingSource=existing_mapping_source,
|
||||
MappingTarget=tool.Ifc.get().create_entity(
|
||||
"IfcCartesianTransformationOperator3D",
|
||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1., 0., 0.)),
|
||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 1., 0.)),
|
||||
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0., 0., 0.)),
|
||||
Scale=1.,
|
||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 0., 1.))
|
||||
)
|
||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
|
||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)),
|
||||
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
|
||||
Scale=1.0,
|
||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
|
||||
),
|
||||
)
|
||||
mapped_representation = tool.Ifc.get().create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
ContextOfItems=context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="MappedRepresentation",
|
||||
Items=[new_mapped_item]
|
||||
Items=[new_mapped_item],
|
||||
)
|
||||
else:
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
tool.Ifc.get(), representation=representation_to_use
|
||||
)
|
||||
|
||||
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ import bonsai.core.geometry
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import cos, pi
|
||||
from math import cos, sin, pi, acos, degrees
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
@@ -225,16 +225,17 @@ class DumbSlabPlaner:
|
||||
for inverse in tool.Ifc.get().get_inverse(layer_set):
|
||||
if not inverse.is_a("IfcMaterialLayerSetUsage") or inverse.LayerSetDirection != "AXIS3":
|
||||
continue
|
||||
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
for rel in tool.Ifc.get().get_inverse(inverse):
|
||||
if not rel.is_a("IfcRelAssociatesMaterial"):
|
||||
continue
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, total_thickness)
|
||||
self.change_thickness(element, total_thickness, preserve_offset=True)
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, total_thickness)
|
||||
self.change_thickness(element, total_thickness, preserve_offset=True)
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
relating_type = settings["relating_type"]
|
||||
@@ -276,9 +277,10 @@ class DumbSlabPlaner:
|
||||
return
|
||||
self.change_thickness(element, total_thickness)
|
||||
|
||||
def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None:
|
||||
def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float, preserve_offset: bool = False) -> None:
|
||||
if tool.Model.get_usage_type(element) != "LAYER3":
|
||||
return
|
||||
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
ifc_file = tool.Ifc.get()
|
||||
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
@@ -296,72 +298,48 @@ class DumbSlabPlaner:
|
||||
if representation:
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if extrusion:
|
||||
# TODO Right now we don't have a reliable way to calculate the existing x_angle only based solely on the extrusion direction.
|
||||
# For instances, a 30 degrees angled extrusion with positive direction has the same extrusion direction as a
|
||||
# -150 degrees angled extrusion with negative direction. The difference lies in the object's rotation.
|
||||
# This means that things can get messy if the user changes the object x angle somehow. We have to figure out an alternative approach.
|
||||
existing_x_angle = obj.rotation_euler.x
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2 * pi, tolerance=0.001) else existing_x_angle
|
||||
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
offset_direction = direction_ratios.copy()
|
||||
perpendicular_depth = thickness * abs(1 / cos(existing_x_angle))
|
||||
perpendicular_offset = layer_offset * abs(1 / cos(existing_x_angle)) / self.unit_scale
|
||||
|
||||
# Calculate the actual extrusion angle from vertical
|
||||
extrusion_angle = 0
|
||||
if direction_ratios.length > 0:
|
||||
cos_angle = direction_ratios.normalized().dot(Vector((0, 0, 1)))
|
||||
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
||||
|
||||
# Check angle and z direction to determine whether the extrusion direction is positive or negative
|
||||
if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or (
|
||||
abs(existing_x_angle) > (pi / 2) and direction_ratios.z < 0
|
||||
):
|
||||
# The extrusion direction is positive. If the layer_parameter is set to negative,
|
||||
# then the we change the extrusion direction.
|
||||
if layer_params["direction_sense"] == "NEGATIVE":
|
||||
direction_ratios *= -1
|
||||
elif (abs(existing_x_angle) > (pi / 2) and direction_ratios.z > 0) or (
|
||||
abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0
|
||||
):
|
||||
# The extrusion direction is negative. If the layer_parameter is set to positive,
|
||||
# then the we change the extrusion direction. And the offset direction should remain positive
|
||||
# for either direction sense, so we change it.
|
||||
offset_direction *= -1
|
||||
if layer_params["direction_sense"] == "POSITIVE":
|
||||
direction_ratios *= -1
|
||||
# Only apply 1/cos factor when there's actual extrusion slope
|
||||
if extrusion_angle > 1e-6:
|
||||
perpendicular_depth = thickness * abs(1 / cos(extrusion_angle))
|
||||
perpendicular_offset = layer_offset * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
perpendicular_depth = thickness
|
||||
perpendicular_offset = layer_offset
|
||||
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
|
||||
extrusion.Depth = perpendicular_depth
|
||||
|
||||
# Update position
|
||||
ifc_position = extrusion.Position
|
||||
position = offset_direction * perpendicular_offset
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material:
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
material.OffsetFromReferenceLine = position.z
|
||||
if ifc_position:
|
||||
ifc_position.Location.Coordinates = position
|
||||
else:
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
|
||||
else:
|
||||
props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
new_rep = ifcopenshell.api.geometry.add_slab_representation(
|
||||
tool.Ifc.get(),
|
||||
context=body_context,
|
||||
depth=thickness * self.unit_scale,
|
||||
x_angle=x_angle,
|
||||
)
|
||||
for inverse in tool.Ifc.get().get_inverse(representation):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, representation, new_rep)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_rep,
|
||||
)
|
||||
bonsai.core.geometry.remove_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
)
|
||||
return
|
||||
if direction_ratios.length > 0:
|
||||
offset_vector = direction_ratios.normalized() * perpendicular_offset
|
||||
position = offset_vector
|
||||
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
# Only set offset if not preserving it (preserves independent offsets per instance)
|
||||
if not preserve_offset:
|
||||
material.OffsetFromReferenceLine = position.z
|
||||
|
||||
if ifc_position:
|
||||
ifc_position.Location.Coordinates = position
|
||||
else:
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
else:
|
||||
props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
@@ -374,13 +352,118 @@ class DumbSlabPlaner:
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=element, representation=representation
|
||||
)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
def update_extrusion_direction(
|
||||
element: ifcopenshell.entity_instance, new_direction_ratios: tuple, obj: bpy.types.Object = None
|
||||
) -> None:
|
||||
"""
|
||||
Update extrusion direction while preserving overall object orientation.
|
||||
|
||||
Args:
|
||||
element: The IFC element
|
||||
new_direction_ratios: New extrusion direction ratios (x,y,z)
|
||||
obj: Optional Blender object (will be fetched if not provided)
|
||||
"""
|
||||
if not obj:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
|
||||
# Get current extrusion direction
|
||||
old_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
if old_direction.length == 0:
|
||||
old_direction = Vector((0, 0, 1)) # Default
|
||||
|
||||
new_direction = Vector(new_direction_ratios)
|
||||
if new_direction.length == 0:
|
||||
new_direction = Vector((0, 0, 1)) # Default
|
||||
|
||||
# Normalize both directions
|
||||
old_direction_normalized = old_direction.normalized()
|
||||
new_direction_normalized = new_direction.normalized()
|
||||
|
||||
# Store current object matrix
|
||||
old_matrix = obj.matrix_world.copy()
|
||||
|
||||
# Calculate the rotation needed to keep same orientation
|
||||
# When extrusion direction changes from A to B relative to local coordinates,
|
||||
# we need to rotate the object by the inverse of that change
|
||||
|
||||
# Calculate rotation from old to new direction
|
||||
rotation_axis = old_direction_normalized.cross(new_direction_normalized)
|
||||
if rotation_axis.length > 1e-6:
|
||||
rotation_axis.normalized()
|
||||
dot_product = old_direction_normalized.dot(new_direction_normalized)
|
||||
angle = acos(min(max(dot_product, -1), 1))
|
||||
|
||||
# Apply INVERSE rotation to object to compensate
|
||||
rotation_matrix = Matrix.Rotation(-angle, 4, rotation_axis)
|
||||
|
||||
# Update object rotation
|
||||
obj.matrix_world = old_matrix @ rotation_matrix
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Update extrusion direction (keeping magnitude)
|
||||
if old_direction.length > 0:
|
||||
# Preserve the magnitude of the original direction vector
|
||||
magnitude = old_direction.length
|
||||
new_direction = new_direction_normalized * magnitude
|
||||
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_direction)
|
||||
|
||||
# Update depth based on new extrusion angle
|
||||
extrusion_angle = 0
|
||||
if new_direction.length > 0:
|
||||
cos_angle = new_direction_normalized.dot(Vector((0, 0, 1)))
|
||||
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
||||
|
||||
# Get current depth (perpendicular depth)
|
||||
current_perpendicular_depth = extrusion.Depth
|
||||
|
||||
# If we have material layer info, calculate actual thickness
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
actual_thickness = current_perpendicular_depth
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
layer_set = material.ForLayerSet
|
||||
actual_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
actual_thickness *= unit_scale
|
||||
|
||||
# Convert to perpendicular depth if needed
|
||||
if extrusion_angle > 1e-6:
|
||||
new_perpendicular_depth = actual_thickness * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
new_perpendicular_depth = actual_thickness
|
||||
|
||||
extrusion.Depth = new_perpendicular_depth
|
||||
|
||||
# Update position offset if needed
|
||||
if extrusion.Position:
|
||||
# Recalculate offset based on new direction
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||
offset = material.OffsetFromReferenceLine
|
||||
if extrusion_angle > 1e-6:
|
||||
perpendicular_offset = offset * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
perpendicular_offset = offset
|
||||
|
||||
offset_vector = new_direction_normalized * perpendicular_offset
|
||||
extrusion.Position.Location.Coordinates = tuple(offset_vector)
|
||||
|
||||
|
||||
class EnableEditingSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -657,6 +740,8 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
|
||||
if extrusion.Position:
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
position.translation *= self.unit_scale
|
||||
@@ -669,22 +754,49 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
# Restore Object rotation to zero
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(-existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
# For AXIS3 with dual rotation: Reset rotation to zero so profile is horizontal
|
||||
if usage_type == "LAYER3":
|
||||
# Store original rotation for later restoration
|
||||
original_rotation_x = obj.rotation_euler.x
|
||||
obj["pre_edit_rotation_x"] = original_rotation_x
|
||||
|
||||
# Reset rotation to zero - profile will be horizontal
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
obj.rotation_euler.x = 0.0
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
else:
|
||||
# Original behavior: Restore Object rotation to zero
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(-existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=existing_x_angle)
|
||||
# Import profile with correct x_angle
|
||||
if usage_type == "LAYER3":
|
||||
# For LAYER3: Use x_angle=0 and scale by cos(rotation) to get horizontal projection
|
||||
obj_x_rotation = original_rotation_x # Use stored original rotation
|
||||
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||
|
||||
# Import with x_angle=0
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=0)
|
||||
|
||||
# Scale the Y coordinates by cos(rotation) to get horizontal projection
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
for vert in obj.data.vertices:
|
||||
vert.co.y *= scale_factor
|
||||
else:
|
||||
# For other types: Use existing_x_angle
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, x_angle=existing_x_angle)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context))
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -706,6 +818,8 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
|
||||
if extrusion.Position:
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
position.translation *= self.unit_scale
|
||||
@@ -718,17 +832,38 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
# Restore Object rotation to x_angle
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
# Restore rotation
|
||||
if usage_type == "LAYER3":
|
||||
# Restore original rotation from before editing
|
||||
if "pre_edit_rotation_x" in obj:
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
obj.rotation_euler.x = obj["pre_edit_rotation_x"]
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
del obj["pre_edit_rotation_x"]
|
||||
else:
|
||||
# Original behavior
|
||||
local_rot_mat = obj.rotation_euler.to_matrix()
|
||||
rot_mat = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
new_rot_mat = local_rot_mat.to_4x4() @ rot_mat
|
||||
new_rot_euler = new_rot_mat.to_euler()
|
||||
obj.rotation_euler = new_rot_euler
|
||||
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
||||
# Export profile with correct x_angle
|
||||
if usage_type == "LAYER3":
|
||||
# Scale Y coordinates back up before exporting
|
||||
obj_x_rotation = obj.rotation_euler.x
|
||||
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||
|
||||
# Un-scale the profile before exporting
|
||||
for vert in obj.data.vertices:
|
||||
vert.co.y /= scale_factor # Inverse of import scaling
|
||||
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=0)
|
||||
else:
|
||||
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
||||
|
||||
if not profile:
|
||||
|
||||
@@ -780,6 +915,28 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Ifc.get(), product=element, representation=new_footprint
|
||||
)
|
||||
|
||||
footprint_context = ifcopenshell.util.representation.get_context(
|
||||
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
if not footprint_context:
|
||||
return
|
||||
|
||||
curves = [profile.OuterCurve]
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
curves.extend(profile.InnerCurves)
|
||||
new_footprint = ifcopenshell.api.geometry.add_footprint_representation(
|
||||
tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint)
|
||||
else:
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
tool.Ifc.get(), product=element, representation=new_footprint
|
||||
)
|
||||
|
||||
|
||||
class ResetVertex(bpy.types.Operator):
|
||||
bl_idname = "bim.reset_vertex"
|
||||
|
||||
@@ -43,7 +43,7 @@ import bonsai.core.geometry
|
||||
import bonsai.core.model as core
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import pi, sin, cos, degrees, atan2
|
||||
from math import pi, sin, cos, degrees, atan2, acos
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator
|
||||
@@ -397,27 +397,46 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for obj in selected_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
continue
|
||||
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
continue
|
||||
|
||||
# Get extrusion direction
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
|
||||
# Calculate angle from vertical
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
|
||||
|
||||
# For sloped walls, compensate so VERTICAL height = target depth
|
||||
cos_angle = cos(x_angle)
|
||||
compensation_factor = abs(1 / cos_angle) if abs(cos_angle) > 1e-6 else 1.0
|
||||
new_depth_ifc = (self.depth / si_conversion) * compensation_factor
|
||||
|
||||
extrusion.Depth = new_depth_ifc
|
||||
|
||||
# IMPORTANT: Refresh the geometry to reflect the IFC changes
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements":
|
||||
ifcopenshell.api.geometry.disconnect_element(
|
||||
ifc_file,
|
||||
relating_element=rel.RelatingElement,
|
||||
related_element=element,
|
||||
)
|
||||
layer2_objs.append(obj)
|
||||
related_element = rel.RelatedElement
|
||||
if related_element.is_a() == "IfcWall":
|
||||
layer2_objs.append(tool.Ifc.get_object(related_element))
|
||||
|
||||
if layer2_objs:
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -437,81 +456,143 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
layer2_objs: list[bpy.types.Object] = []
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
x_angle = self.x_angle
|
||||
|
||||
for obj in selected_objs:
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
if not element:
|
||||
continue
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
continue
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
continue
|
||||
|
||||
# Get current object rotation matrix
|
||||
obj_rotation = obj.matrix_world.to_3x3()
|
||||
|
||||
# Get current extrusion direction in LOCAL coordinates
|
||||
current_local_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
if current_local_direction.length == 0:
|
||||
current_local_direction = Vector((0, 0, 1))
|
||||
current_local_direction_normalized = current_local_direction.normalized()
|
||||
|
||||
# Calculate what the current extrusion direction is in WORLD coordinates
|
||||
current_world_direction = obj_rotation @ current_local_direction_normalized
|
||||
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
|
||||
# Calculate the NEW local extrusion direction based on x_angle
|
||||
new_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
|
||||
|
||||
# Check if extrusion direction is actually changing
|
||||
current_local_norm = current_local_direction_normalized
|
||||
new_local_norm = new_local_direction.normalized()
|
||||
|
||||
# Compare the LOCAL directions
|
||||
local_direction_changed = (new_local_norm - current_local_norm).length > 1e-6
|
||||
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
depth = extrusion.Depth / abs(1 / cos(existing_x_angle))
|
||||
perpendicular_depth = depth * abs(1 / cos(x_angle))
|
||||
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
|
||||
layer2_objs.append(obj)
|
||||
|
||||
# Update extrusion direction
|
||||
if local_direction_changed:
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_local_direction)
|
||||
|
||||
# Always update depth
|
||||
extrusion.Depth = perpendicular_depth
|
||||
layer2_objs.append(obj)
|
||||
|
||||
else:
|
||||
if tool.Model.get_usage_type(element) == "LAYER3":
|
||||
existing_x_angle = obj.rotation_euler.x
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
# For slabs, handle polyline scaling
|
||||
existing_obj_x_angle = obj.rotation_euler.x
|
||||
existing_obj_x_angle = (
|
||||
0 if tool.Cad.is_x(existing_obj_x_angle, 0, tolerance=0.001) else existing_obj_x_angle
|
||||
)
|
||||
existing_obj_x_angle = (
|
||||
0 if tool.Cad.is_x(existing_obj_x_angle, pi, tolerance=0.001) else existing_obj_x_angle
|
||||
)
|
||||
|
||||
# Scale the polyline coordinates
|
||||
coord_list = builder.get_polyline_coords(extrusion.SweptArea.OuterCurve)
|
||||
coord_list = [
|
||||
(p[0], p[1] * abs(cos(existing_x_angle))) for p in coord_list
|
||||
] # Reset the transformation and returns to the original points with 0 degrees
|
||||
] # Reset the transformation
|
||||
coord_list = [
|
||||
(p[0], p[1] * abs(1 / cos(x_angle))) for p in coord_list
|
||||
] # Apply the transformation for the new x_angle
|
||||
builder.set_polyline_coords(extrusion.SweptArea.OuterCurve, coord_list)
|
||||
|
||||
# The extrusion direction calculated previously default to the positive direction
|
||||
# Here we set the extrusion direction to negative if that's the case
|
||||
direction_ratios = Vector((0.0, sin(x_angle), cos(x_angle)))
|
||||
# direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||
# Calculate new extrusion direction with direction sense
|
||||
base_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale
|
||||
perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale
|
||||
offset_direction = direction_ratios.copy()
|
||||
offset_direction = base_local_direction.copy()
|
||||
|
||||
# Check angle and z direction to determine whether the extrusion direction is positive or negative
|
||||
if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or (
|
||||
abs(x_angle) > (pi / 2) and direction_ratios.z < 0
|
||||
# Apply direction sense
|
||||
final_local_direction = base_local_direction.copy()
|
||||
if (abs(x_angle) < (pi / 2) and base_local_direction.z > 0) or (
|
||||
abs(x_angle) > (pi / 2) and base_local_direction.z < 0
|
||||
):
|
||||
# The extrusion direction is positive. If the layer_parameter is set to negative,
|
||||
# then the we change the extrusion direction.
|
||||
if layer_params["direction_sense"] == "NEGATIVE":
|
||||
direction_ratios *= -1
|
||||
elif ((x_angle) > (pi / 2) and direction_ratios.z > 0) or (
|
||||
(x_angle) < (pi / 2) and direction_ratios.z < 0
|
||||
final_local_direction *= -1
|
||||
elif (x_angle > (pi / 2) and base_local_direction.z > 0) or (
|
||||
x_angle < (pi / 2) and base_local_direction.z < 0
|
||||
):
|
||||
# The extrusion direction is negative. If the layer_parameter is set to positive,
|
||||
# then the we change the extrusion direction.
|
||||
# then the we change the extrusion direction. And the offset direction should remain positive
|
||||
# for either direction sense, so we change it.
|
||||
offset_direction *= -1
|
||||
if layer_params["direction_sense"] == "POSITIVE":
|
||||
direction_ratios *= -1
|
||||
final_local_direction *= -1
|
||||
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios)
|
||||
# Check if extrusion direction actually changed
|
||||
final_local_norm = final_local_direction.normalized()
|
||||
local_direction_changed = (final_local_norm - current_local_norm).length > 1e-6
|
||||
|
||||
# Update extrusion properties
|
||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(final_local_direction)
|
||||
extrusion.Depth = perpendicular_depth
|
||||
|
||||
if extrusion.Position or perpendicular_offset != 0:
|
||||
position = offset_direction * perpendicular_offset
|
||||
tool.Model.add_extrusion_position(extrusion, position)
|
||||
|
||||
# Adjust object rotation if extrusion direction changed
|
||||
if local_direction_changed:
|
||||
# Calculate what the NEW world direction would be with current object rotation
|
||||
expected_new_world_direction = obj_rotation @ final_local_norm
|
||||
|
||||
# The rotation needed is from expected_new_world_direction to current_world_direction
|
||||
rotation_axis = expected_new_world_direction.cross(current_world_direction)
|
||||
if rotation_axis.length > 1e-6:
|
||||
rotation_axis.normalize()
|
||||
dot_product = expected_new_world_direction.dot(current_world_direction)
|
||||
angle = acos(min(max(dot_product, -1), 1))
|
||||
|
||||
# Rotate around object's own origin
|
||||
# Decompose the matrix to get translation, rotation, scale
|
||||
translation, rotation, scale = obj.matrix_world.decompose()
|
||||
|
||||
# Create rotation matrix and convert to quaternion
|
||||
rotation_matrix = Matrix.Rotation(angle, 4, rotation_axis)
|
||||
rotation_quat = rotation_matrix.to_quaternion()
|
||||
|
||||
# Apply rotation to existing rotation (quaternion multiplication)
|
||||
new_rotation = rotation_quat @ rotation
|
||||
|
||||
# Reconstruct matrix_world with same translation, new rotation, same scale
|
||||
obj.matrix_world = (
|
||||
Matrix.Translation(translation) @ new_rotation.to_matrix().to_4x4() @ Matrix.Scale(1, 4)
|
||||
)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
@@ -519,12 +600,6 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
# Object rotation
|
||||
current_z_rot = obj.rotation_euler.z
|
||||
rot_mat = mathutils.Matrix.Rotation(x_angle, 4, "X")
|
||||
obj.rotation_euler = rot_mat.to_euler()
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
|
||||
if layer2_objs:
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
@@ -1022,6 +1097,7 @@ class DumbWallGenerator:
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
|
||||
@@ -399,6 +399,16 @@ class EditItemUI:
|
||||
assert obj
|
||||
|
||||
mesh_props = tool.Geometry.get_mesh_props(obj.data)
|
||||
|
||||
# Get the parent element from representation_obj to check for layer set usage
|
||||
has_layer_set_usage = False
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if props.representation_obj:
|
||||
parent_element = tool.Ifc.get_entity(props.representation_obj)
|
||||
if parent_element:
|
||||
material_usage = tool.Model.get_usage_type(parent_element)
|
||||
has_layer_set_usage = material_usage == "LAYER3"
|
||||
|
||||
if AuthoringData.data["is_representation_item_swept_solid"]:
|
||||
# TODO: support EndSweptArea for IfcRevolvedAreaSolidTapered,
|
||||
# will need to add second attribute for this.
|
||||
@@ -412,8 +422,12 @@ class EditItemUI:
|
||||
op.profile_id = int(mesh_props.item_profile)
|
||||
|
||||
for item_attribute in mesh_props.item_attributes:
|
||||
# Skip depth attribute for LAYER3 objects with layer set usage
|
||||
if has_layer_set_usage and item_attribute.name.lower() == "depth":
|
||||
continue
|
||||
row = cls.layout.row()
|
||||
draw_attribute(item_attribute, cls.layout)
|
||||
|
||||
if len(mesh_props.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]:
|
||||
row = cls.layout.row()
|
||||
row.operator("bim.update_item_attributes", icon="FILE_REFRESH", text="")
|
||||
|
||||
@@ -1062,18 +1062,34 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
and not self.is_advanced
|
||||
):
|
||||
filepath = self.get_filepath()
|
||||
suffix = tool.Blender.get_addon_preferences().metadata_blend_file_suffix
|
||||
if str(filepath).lower().endswith(".ifc"):
|
||||
metadata_path = Path(str(filepath)[:-4] + suffix)
|
||||
else:
|
||||
metadata_path = Path(str(filepath) + suffix)
|
||||
if metadata_path.exists() and metadata_path.is_file():
|
||||
try:
|
||||
bpy.ops.bim.load_blend_metadata_and_ifc(filepath=filepath)
|
||||
self.report({"INFO"}, f"Loaded metadata file: {metadata_path.name}")
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"WARNING"}, f"Failed to load metadata file, using regular load: {e}")
|
||||
|
||||
# First, load the IFC file temporarily to check for metadata document
|
||||
temp_ifc = None
|
||||
has_metadata_doc = False
|
||||
try:
|
||||
temp_ifc = ifcopenshell.open(str(filepath))
|
||||
for doc in temp_ifc.by_type("IfcDocumentInformation"):
|
||||
if getattr(doc, "Scope", None) == "BLEND_METADATA":
|
||||
has_metadata_doc = True
|
||||
break
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
temp_ifc = None
|
||||
|
||||
if has_metadata_doc:
|
||||
suffix = tool.Blender.get_addon_preferences().metadata_blend_file_suffix
|
||||
if str(filepath).lower().endswith(".ifc"):
|
||||
metadata_path = Path(str(filepath)[:-4] + suffix)
|
||||
else:
|
||||
metadata_path = Path(str(filepath) + suffix)
|
||||
if metadata_path.exists() and metadata_path.is_file():
|
||||
try:
|
||||
bpy.ops.bim.load_blend_metadata_and_ifc(filepath=filepath)
|
||||
self.report({"INFO"}, f"Loaded metadata file: {metadata_path.name}")
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"WARNING"}, f"Failed to load metadata file, using regular load: {e}")
|
||||
|
||||
@persistent
|
||||
def load_handler(*args):
|
||||
@@ -1121,6 +1137,10 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
props.is_loading = True
|
||||
props.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
||||
props.use_relative_project_path = self.use_relative_path
|
||||
|
||||
metadata_doc = tool.Project.get_metadata_document_information()
|
||||
props.should_save_metadata_for_this_file = metadata_doc is not None
|
||||
|
||||
tool.Blender.register_toolbar()
|
||||
tool.Project.add_recent_ifc_project(self.get_filepath_abs())
|
||||
|
||||
@@ -1749,6 +1769,22 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
settings.json_version = self.json_version
|
||||
settings.json_compact = self.json_compact
|
||||
|
||||
pprops = tool.Project.get_project_props()
|
||||
if tool.Blender.get_addon_preferences().save_metadata_blend_file and pprops.should_save_metadata_for_this_file:
|
||||
suffix = tool.Blender.get_addon_preferences().metadata_blend_file_suffix
|
||||
if output_file.lower().endswith(".ifc"):
|
||||
metadata_filename = os.path.basename(output_file)[:-4] + suffix
|
||||
else:
|
||||
metadata_filename = os.path.basename(output_file) + suffix
|
||||
|
||||
if not tool.Project.get_metadata_document_information():
|
||||
tool.Project.create_metadata_document_information(metadata_filename)
|
||||
else:
|
||||
tool.Project.update_metadata_document_information(metadata_filename)
|
||||
else:
|
||||
if not pprops.should_save_metadata_for_this_file:
|
||||
tool.Project.remove_metadata_document_information()
|
||||
|
||||
ifc_exporter = export_ifc.IfcExporter(settings)
|
||||
print("Starting export")
|
||||
settings.logger.info("Starting export")
|
||||
@@ -1765,7 +1801,8 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
tool.Ifc.set_path(output_file)
|
||||
bim_props.is_dirty = False
|
||||
|
||||
if tool.Blender.get_addon_preferences().save_metadata_blend_file:
|
||||
pprops = tool.Project.get_project_props()
|
||||
if tool.Blender.get_addon_preferences().save_metadata_blend_file and pprops.should_save_metadata_for_this_file:
|
||||
try:
|
||||
bpy.ops.bim.save_blend_metadata_file()
|
||||
suffix = tool.Blender.get_addon_preferences().metadata_blend_file_suffix
|
||||
@@ -3128,6 +3165,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
||||
bl_idname = "bim.load_blend_metadata_and_ifc"
|
||||
bl_label = "Load Blend Metadata and IFC"
|
||||
@@ -3165,4 +3203,4 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
||||
|
||||
bpy.app.handlers.load_post.append(load_handler)
|
||||
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
||||
return {"FINISHED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -411,6 +411,11 @@ class BIMProjectProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
|
||||
should_save_metadata_for_this_file: BoolProperty(
|
||||
name="Save Session Data for This File",
|
||||
description="Enable saving session data (window layout, settings) to a metadata blend file for this specific IFC file",
|
||||
default=False,
|
||||
)
|
||||
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
|
||||
queried_obj_root: bpy.props.PointerProperty(type=bpy.types.Object)
|
||||
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
@@ -504,6 +509,7 @@ class BIMProjectProperties(PropertyGroup):
|
||||
parent_library: str
|
||||
|
||||
use_relative_project_path: bool
|
||||
should_save_metadata_for_this_file: bool
|
||||
queried_obj: Union[bpy.types.Object, None]
|
||||
queried_obj_root: Union[bpy.types.Object, None]
|
||||
clipping_planes: bpy.types.bpy_prop_collection_idprop[ObjProperty]
|
||||
|
||||
@@ -338,9 +338,9 @@ class BIM_PT_project(Panel):
|
||||
else:
|
||||
metadata_filename = os.path.basename(props.ifc_file) + suffix
|
||||
row = self.layout.row(align=True)
|
||||
col = row.column()
|
||||
col.enabled = False
|
||||
col.label(text=f"Saving session data to: {metadata_filename}")
|
||||
row.use_property_split = False
|
||||
pprops = tool.Project.get_project_props()
|
||||
row.prop(pprops, "should_save_metadata_for_this_file", text=f"Save session data to: {metadata_filename}")
|
||||
|
||||
|
||||
class BIM_PT_new_project_wizard(Panel):
|
||||
|
||||
@@ -175,31 +175,84 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
return
|
||||
|
||||
def get_root_aggregate(element):
|
||||
"""Traverse up the aggregate hierarchy to find the top-most aggregate"""
|
||||
current = element
|
||||
root = None
|
||||
while aggregate := ifcopenshell.util.element.get_aggregate(current):
|
||||
root = aggregate
|
||||
current = aggregate
|
||||
return root
|
||||
|
||||
def get_all_parts_recursive(element):
|
||||
"""Recursively get all parts of an aggregate"""
|
||||
parts = []
|
||||
for part in ifcopenshell.util.element.get_parts(element):
|
||||
parts.append(part)
|
||||
# Recursively get nested parts
|
||||
parts.extend(get_all_parts_recursive(part))
|
||||
return parts
|
||||
|
||||
objs: list[bpy.types.Object] = []
|
||||
# In IFC element can be either contained of aggregated,
|
||||
# tehrefore we skip aggregated elements here to prevent confusion.
|
||||
# Can't handle it in `poll` since user might just select bunch of elements
|
||||
# and try to assign a container to them
|
||||
# and excluding aggregates because of the `poll` failing might get awkward.
|
||||
skipped_aggregates = 0
|
||||
processed_elements = set() # Track elements we've already handled (by IFC ID)
|
||||
promoted_parts = 0 # Count how many parts were promoted to their root aggregate
|
||||
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_aggregate(element):
|
||||
skipped_aggregates += 1
|
||||
continue
|
||||
objs.append(obj)
|
||||
|
||||
# Check if element is part of an aggregate (at any level)
|
||||
if root_aggregate := get_root_aggregate(element):
|
||||
# Skip if we've already processed this root aggregate
|
||||
if root_aggregate.id() in processed_elements:
|
||||
continue
|
||||
|
||||
# Get the root aggregate object and add it instead
|
||||
if root_aggregate_obj := tool.Ifc.get_object(root_aggregate):
|
||||
objs.append(root_aggregate_obj)
|
||||
processed_elements.add(root_aggregate.id())
|
||||
if root_aggregate != element: # Only count as promoted if different from selected
|
||||
promoted_parts += 1
|
||||
else:
|
||||
# Element is not part of any aggregate
|
||||
if element.id() not in processed_elements:
|
||||
objs.append(obj)
|
||||
processed_elements.add(element.id())
|
||||
|
||||
# Get the container's collection
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
container_collection = container_obj.BIMObjectProperties.collection if container_obj else None
|
||||
|
||||
for element_obj in objs:
|
||||
element = tool.Ifc.get_entity(element_obj)
|
||||
|
||||
# Only assign container to the ROOT aggregate (this updates IFC relationships)
|
||||
if self.remove_from_other_containers:
|
||||
for col in element_obj.users_collection[:]:
|
||||
col.objects.unlink(element_obj)
|
||||
core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj)
|
||||
|
||||
aggregates_msg = ""
|
||||
if skipped_aggregates:
|
||||
aggregates_msg = f" {skipped_aggregates} aggregated elements skipped."
|
||||
self.report({"INFO"}, f"{len(objs)} elements assigned.{aggregates_msg}")
|
||||
# For parts, only move them in Blender collections (don't change IFC relationships)
|
||||
if container_collection:
|
||||
all_parts = get_all_parts_recursive(element)
|
||||
for part in all_parts:
|
||||
if part_obj := tool.Ifc.get_object(part):
|
||||
# Always remove from ALL previous collections when moving to new container
|
||||
for col in part_obj.users_collection[:]:
|
||||
col.objects.unlink(part_obj)
|
||||
|
||||
# Link to new container collection (Blender-only, no IFC change)
|
||||
if part_obj.name not in container_collection.objects:
|
||||
container_collection.objects.link(part_obj)
|
||||
|
||||
# Disable editing mode for all selected objects
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
core.disable_editing_container(tool.Spatial, obj=obj)
|
||||
|
||||
promoted_msg = ""
|
||||
if promoted_parts:
|
||||
promoted_msg = f" {promoted_parts} nested parts promoted to their root aggregates."
|
||||
self.report({"INFO"}, f"{len(objs)} elements assigned.{promoted_msg}")
|
||||
|
||||
|
||||
class EnableEditingContainer(bpy.types.Operator):
|
||||
|
||||
@@ -98,6 +98,8 @@ def update_name(self: "BIMContainer", context: bpy.types.Context) -> None:
|
||||
tool.Spatial.edit_container_name(element, self.name)
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
tool.Root.set_object_name(obj, element)
|
||||
if collection := tool.Blender.get_object_bim_props(obj).collection:
|
||||
collection.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ class SelectSimilarType(bpy.types.Operator):
|
||||
for related_object in objects:
|
||||
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(related_object))
|
||||
if not relating_type:
|
||||
related_object.select_set(False)
|
||||
# Keep objects without a type selected (retain current selection)
|
||||
continue
|
||||
relating_types.add(relating_type)
|
||||
|
||||
|
||||
@@ -33,15 +33,6 @@ import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.handler
|
||||
from enum import Enum
|
||||
from bonsai.bim.helper import (
|
||||
get_all_tab_panels,
|
||||
get_tab_visibility,
|
||||
set_tab_visibility,
|
||||
get_tab_names,
|
||||
get_panel_config,
|
||||
initialize_panel_properties,
|
||||
initialize_tab_visibilities,
|
||||
)
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bonsai.bim import import_ifc
|
||||
from bonsai.bim.prop import StrProperty
|
||||
@@ -364,8 +355,6 @@ bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}')
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
|
||||
# TODO: Unused operator.
|
||||
# Is there a need for this or 'DIR_PATH' propety subtype does almost the same,
|
||||
# but also has alt+click?
|
||||
@@ -1737,156 +1726,6 @@ class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_UL_tab_panels(bpy.types.UIList):
|
||||
"""UIList for Tab Panels"""
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item["bl_label"])
|
||||
|
||||
row.operator(
|
||||
"bim.toggle_panel_visibility",
|
||||
text="",
|
||||
icon="HIDE_OFF" if item.get("visible", True) else "HIDE_ON",
|
||||
).action = f"TOGGLE_VISIBILITY_{item.name}"
|
||||
|
||||
row.operator(
|
||||
"bim.bookmark_panel",
|
||||
text="",
|
||||
icon="SOLO_ON" if item.get("bookmarked", False) else "SOLO_OFF",
|
||||
).action = f"BOOKMARK_{item.name}"
|
||||
|
||||
|
||||
class BIM_OT_toggle_panel_visibility(bpy.types.Operator):
|
||||
"""Toggle Panel Visibility"""
|
||||
|
||||
bl_idname = "bim.toggle_panel_visibility"
|
||||
bl_label = "Toggle Panel Visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
action: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
panel_name = self.action.replace("TOGGLE_VISIBILITY_", "")
|
||||
active_tab = getattr(context.scene, "active_tab_name", None) or getattr(
|
||||
tool.Blender.get_bim_props(), "tab", None
|
||||
)
|
||||
is_bookmark_tab = active_tab == "BOOKMARK"
|
||||
|
||||
panel_config = get_panel_config(panel_name, create_if_missing=True)
|
||||
if panel_config:
|
||||
if is_bookmark_tab:
|
||||
panel_config.is_visible_in_bookmarks = not panel_config.is_visible_in_bookmarks
|
||||
new_value = panel_config.is_visible_in_bookmarks
|
||||
else:
|
||||
panel_config.is_visible_in_tab = not panel_config.is_visible_in_tab
|
||||
new_value = panel_config.is_visible_in_tab
|
||||
|
||||
for item in context.scene.tab_panels:
|
||||
if item.name == panel_name:
|
||||
item["visible"] = new_value
|
||||
break
|
||||
|
||||
for area in bpy.context.window.screen.areas:
|
||||
if area.type == "PROPERTIES":
|
||||
area.tag_redraw()
|
||||
|
||||
tab_context = "Bookmarks" if is_bookmark_tab else "Tab"
|
||||
self.report({"INFO"}, f"Toggled visibility for {panel_name} in {tab_context}.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_bookmark_panel(bpy.types.Operator):
|
||||
"""Bookmark Panel"""
|
||||
|
||||
bl_idname = "bim.bookmark_panel"
|
||||
bl_label = "Bookmark Panel"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
action: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
panel_name = self.action.replace("BOOKMARK_", "")
|
||||
panel_config = get_panel_config(panel_name, create_if_missing=True)
|
||||
|
||||
if panel_config:
|
||||
panel_config.is_bookmarked = not panel_config.is_bookmarked
|
||||
|
||||
for item in context.scene.tab_panels:
|
||||
if item.name == panel_name:
|
||||
item["bookmarked"] = panel_config.is_bookmarked
|
||||
break
|
||||
|
||||
for area in bpy.context.window.screen.areas:
|
||||
if area.type == "PROPERTIES":
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({"INFO"}, f"Toggled bookmark for {panel_name}.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_manage_tab_panels(bpy.types.Operator):
|
||||
"""Manage Tab Panels"""
|
||||
|
||||
bl_idname = "bim.manage_tab_panels"
|
||||
bl_label = "Manage Tab Panels"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
tab_name: bpy.props.StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
|
||||
context.scene.active_tab_name = self.tab_name
|
||||
context.scene.tab_panels.clear()
|
||||
|
||||
initialize_tab_visibilities()
|
||||
initialize_panel_properties()
|
||||
all_panels = get_all_tab_panels(force_refresh=True)
|
||||
|
||||
for panel_data in all_panels.get(self.tab_name, []):
|
||||
panel_name = panel_data.get("bl_idname", "")
|
||||
panel_label = panel_data.get("bl_label", "")
|
||||
if not panel_name or not panel_label:
|
||||
continue
|
||||
|
||||
item = context.scene.tab_panels.add()
|
||||
item.name = panel_name
|
||||
item["bl_label"] = panel_label
|
||||
|
||||
panel_config = get_panel_config(panel_name, create_if_missing=True)
|
||||
if panel_config:
|
||||
if self.tab_name == "BOOKMARK":
|
||||
item["visible"] = panel_config.is_visible_in_bookmarks
|
||||
else:
|
||||
item["visible"] = panel_config.is_visible_in_tab
|
||||
item["bookmarked"] = panel_config.is_bookmarked
|
||||
else:
|
||||
item["visible"] = True
|
||||
item["bookmarked"] = False
|
||||
|
||||
return context.window_manager.invoke_popup(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text=f"Manage Panels for {self.tab_name} Tab")
|
||||
|
||||
row = layout.row()
|
||||
row.template_list("BIM_UL_tab_panels", "", context.scene, "tab_panels", context.scene, "active_tab_panel_index")
|
||||
|
||||
def execute(self, context):
|
||||
for item in context.scene.tab_panels:
|
||||
panel_config = get_panel_config(item.name, create_if_missing=True)
|
||||
if panel_config:
|
||||
if self.tab_name == "BOOKMARK":
|
||||
panel_config.is_visible_in_bookmarks = item["visible"]
|
||||
else:
|
||||
panel_config.is_visible_in_tab = item["visible"]
|
||||
panel_config.is_bookmarked = item["bookmarked"]
|
||||
|
||||
self.report({"INFO"}, f"Panels for {self.tab_name} managed successfully.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_manage_tab_visibility(bpy.types.Operator):
|
||||
"""Manage Tab Visibility"""
|
||||
|
||||
@@ -1894,51 +1733,26 @@ class BIM_OT_manage_tab_visibility(bpy.types.Operator):
|
||||
bl_label = "Manage Tab Visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
row.operator("bim.reset_ui_layout", icon="FILE_REFRESH", text="")
|
||||
row = layout.row()
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
|
||||
for tab_name in get_tab_names():
|
||||
row = layout.row()
|
||||
row.label(text=tab_name)
|
||||
is_visible = get_tab_visibility(tab_name)
|
||||
icon = "HIDE_OFF" if is_visible else "HIDE_ON"
|
||||
op = row.operator("bim.toggle_tab_visibility", text="", icon=icon)
|
||||
op.tab_name = tab_name
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
from bonsai.bim.prop import get_tab
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self)
|
||||
bprops = tool.Blender.get_bim_props()
|
||||
bprops.tab_visibilities.clear()
|
||||
bprops.panel_visibilities.clear()
|
||||
tabs = [item[0] for item in get_tab(None, None) if item and item[0] != "BLENDER"]
|
||||
for tab in tabs:
|
||||
new = bprops.tab_visibilities.add()
|
||||
new.name = tab
|
||||
|
||||
|
||||
class BIM_OT_toggle_tab_visibility(bpy.types.Operator):
|
||||
"""Toggle Tab Visibility"""
|
||||
|
||||
bl_idname = "bim.toggle_tab_visibility"
|
||||
bl_label = "Toggle Tab Visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
tab_name: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
if self.tab_name in get_tab_names():
|
||||
current_visibility = get_tab_visibility(self.tab_name)
|
||||
set_tab_visibility(self.tab_name, not current_visibility)
|
||||
|
||||
for area in bpy.context.window.screen.areas:
|
||||
if area.type == "PROPERTIES":
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({"INFO"}, f"Toggled visibility for {self.tab_name}.")
|
||||
for attr_name in dir(bpy.types):
|
||||
if attr_name.startswith("BIM_PT_tab_"):
|
||||
panel_class = getattr(bpy.types, attr_name)
|
||||
if not hasattr(panel_class, "bl_idname"):
|
||||
assert False, panel_class
|
||||
new = bprops.panel_visibilities.add()
|
||||
new.name = panel_class.bl_idname
|
||||
new.label = panel_class.bl_label
|
||||
new.tab_name = panel_class.bim_tab_name
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1950,29 +1764,8 @@ class BIM_OT_reset_ui_layout(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
for tab_name in get_tab_names():
|
||||
set_tab_visibility(tab_name, True)
|
||||
|
||||
get_all_tab_panels()["BOOKMARK"] = [{}]
|
||||
|
||||
for tab_name, panels in get_all_tab_panels().items():
|
||||
for panel in panels:
|
||||
panel_name = panel.get("bl_idname", "")
|
||||
if not panel_name:
|
||||
continue
|
||||
|
||||
show_prop_name = f"show_{panel_name.lower()}"
|
||||
if hasattr(context.scene, show_prop_name):
|
||||
setattr(context.scene, show_prop_name, True)
|
||||
|
||||
bookmark_prop_name = f"bookmark_{panel_name.lower()}"
|
||||
if hasattr(context.scene, bookmark_prop_name):
|
||||
setattr(context.scene, bookmark_prop_name, False)
|
||||
|
||||
for area in bpy.context.window.screen.areas:
|
||||
if area.type == "PROPERTIES":
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({"INFO"}, "UI layout reset to default.")
|
||||
bprops = tool.Blender.get_bim_props()
|
||||
bprops.tab_visibilities.clear()
|
||||
bprops.panel_visibilities.clear()
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -60,6 +60,10 @@ def update_tab(self: "BIMAreaProperties", context: bpy.types.Context) -> None:
|
||||
self.previous_tab = self.tab
|
||||
|
||||
|
||||
def update_is_visible(self: "BIMTabVisibility", context: bpy.types.Context) -> None:
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
|
||||
def update_global_tab(self: "BIMTabProperties", context: bpy.types.Context) -> None:
|
||||
tool.Blender.setup_tabs()
|
||||
screen = context.id_data
|
||||
@@ -537,21 +541,24 @@ class BIMTabProperties(PropertyGroup):
|
||||
|
||||
class BIMTabVisibility(PropertyGroup):
|
||||
name: StringProperty(name="Tab Name")
|
||||
is_visible: BoolProperty(name="Is Visible", default=True)
|
||||
is_visible: BoolProperty(name="Is Visible", default=True, update=update_is_visible)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
is_visible: bool
|
||||
|
||||
|
||||
class BIMPanelProperties(PropertyGroup):
|
||||
is_visible_in_tab: BoolProperty(name="Is Visible in Tab", default=True)
|
||||
is_visible_in_bookmarks: BoolProperty(name="Is Visible in Bookmarks", default=True)
|
||||
is_bookmarked: BoolProperty(name="Is Bookmarked", default=False)
|
||||
class BIMPanelVisibility(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
label: StringProperty(name="Label")
|
||||
tab_name: StringProperty(name="Tab Name")
|
||||
is_visible: BoolProperty(name="Is Visible in Tab", default=True, update=update_is_visible)
|
||||
is_bookmarked: BoolProperty(name="Is Bookmarked", default=False, update=update_is_visible)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_visible_in_tab: bool
|
||||
is_visible_in_bookmarks: bool
|
||||
name: str
|
||||
tab_name: str
|
||||
is_visible: bool
|
||||
is_bookmarked: bool
|
||||
|
||||
|
||||
@@ -628,7 +635,6 @@ class BIMProperties(PropertyGroup):
|
||||
name="Mass Unit",
|
||||
default="KILOGRAM",
|
||||
)
|
||||
|
||||
time_unit: EnumProperty(
|
||||
items=[
|
||||
("SECOND", "Second", "Seconds"),
|
||||
@@ -638,9 +644,11 @@ class BIMProperties(PropertyGroup):
|
||||
],
|
||||
name="Time Unit",
|
||||
default="HOUR",
|
||||
)
|
||||
)
|
||||
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
|
||||
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
|
||||
active_tab_visibility_index: IntProperty(name="Active Tab Visibility Index")
|
||||
panel_visibilities: CollectionProperty(type=BIMPanelVisibility, name="Panel Properties")
|
||||
active_panel_visibility_index: IntProperty(name="Active Panel Property Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_dirty: bool
|
||||
@@ -656,8 +664,10 @@ class BIMProperties(PropertyGroup):
|
||||
volume_unit: str
|
||||
mass_unit: str
|
||||
time_unit: str
|
||||
tab_visibilities: bpy.types.bpy_prop_collection[BIMTabVisibility]
|
||||
panel_properties: bpy.types.bpy_prop_collection[BIMPanelProperties]
|
||||
tab_visibilities: bpy.types.bpy_prop_collection_idprop[BIMTabVisibility]
|
||||
active_tab_visibility_index: int
|
||||
panel_visibilities: bpy.types.bpy_prop_collection_idprop[BIMPanelVisibility]
|
||||
active_panel_visibility_index: int
|
||||
|
||||
|
||||
class IfcParameter(PropertyGroup):
|
||||
@@ -791,12 +801,21 @@ class BIMFacet(PropertyGroup):
|
||||
("!*=", "does not contain", ""),
|
||||
],
|
||||
)
|
||||
filter_mode: EnumProperty(
|
||||
items=[
|
||||
("ADD", "Add", "Add results to the current selection"),
|
||||
("SUBTRACT", "Subtract", "Remove results from the current selection"),
|
||||
("FILTER", "Filter", "Filter the current selection"),
|
||||
],
|
||||
default="ADD",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pset: str
|
||||
value: str
|
||||
type: str
|
||||
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
|
||||
filter_mode: Literal["ADD", "SUBTRACT", "FILTER"]
|
||||
|
||||
|
||||
class BIMFilterGroup(PropertyGroup):
|
||||
|
||||
+148
-238
@@ -36,19 +36,6 @@ import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from ifcopenshell.util.file import IfcHeaderExtractor
|
||||
from bonsai.bim.prop import Attribute
|
||||
from bonsai.bim.helper import (
|
||||
get_tab_names,
|
||||
get_panel_tab_name,
|
||||
should_show_panel,
|
||||
get_tab_visibility,
|
||||
set_tab_visibility,
|
||||
get_panel_visibility,
|
||||
is_panel_bookmarked,
|
||||
get_panel_config,
|
||||
get_all_tab_panels,
|
||||
initialize_tab_visibilities,
|
||||
initialize_panel_properties,
|
||||
)
|
||||
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
|
||||
from bonsai.bim.module.pset.prop import IfcProperty
|
||||
from bonsai.bim.module.model.prop import (
|
||||
@@ -255,6 +242,39 @@ class BIM_UL_generic(bpy.types.UIList):
|
||||
layout.label(text="", translate=False)
|
||||
|
||||
|
||||
class BIM_UL_tab_visibilities(bpy.types.UIList):
|
||||
def draw_item(
|
||||
self,
|
||||
context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: bpy.types.PropertyGroup,
|
||||
item: bpy.types.PropertyGroup,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
) -> None:
|
||||
row = layout.row()
|
||||
row.prop(item, "name", text="", emboss=False)
|
||||
row.prop(item, "is_visible", text="", icon="HIDE_OFF" if item.is_visible else "HIDE_ON", emboss=False)
|
||||
|
||||
|
||||
class BIM_UL_panel_visibilities(bpy.types.UIList):
|
||||
def draw_item(
|
||||
self,
|
||||
context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: bpy.types.PropertyGroup,
|
||||
item: bpy.types.PropertyGroup,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
) -> None:
|
||||
row = layout.row()
|
||||
row.prop(item, "label", text="", emboss=False)
|
||||
row.prop(item, "is_visible", text="", icon="HIDE_OFF" if item.is_visible else "HIDE_ON", emboss=False)
|
||||
row.prop(item, "is_bookmarked", text="", icon="SOLO_ON" if item.is_bookmarked else "SOLO_OFF", emboss=False)
|
||||
|
||||
|
||||
class GizmoPreferencesDoor(bpy.types.PropertyGroup):
|
||||
"""Property group for door gizmo visibility settings."""
|
||||
|
||||
@@ -724,11 +744,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
description="Custom suffix for the metadata blend file. Will be appended to the filename (without .ifc).",
|
||||
default=".ifc.metadata.blend",
|
||||
)
|
||||
user_ui_customization: BoolProperty(
|
||||
name="User UI Customization",
|
||||
description="Enable user interface customization features (hide/show tabs and panels, bookmark panels) and save the session settings as part of the metadata blend file",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
svg2pdf_command: str
|
||||
@@ -770,7 +785,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
chain_filter_with_set_operations: bool
|
||||
default_filter_with_set_operations_for_globalid_and_class: bool
|
||||
save_metadata_blend_file: bool
|
||||
user_ui_customization: bool
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
@@ -837,7 +851,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoDoorEdition.dimension_gizmo_props}
|
||||
# Add special gizmos not in dimension_gizmo_props
|
||||
gizmo_prop_names.update(("swing_arc", "flip_arc"))
|
||||
for prop in door_gizmos.__annotations__:
|
||||
try:
|
||||
annotations = door_gizmos.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(door_gizmos).__annotations__
|
||||
for prop in annotations:
|
||||
if prop in gizmo_prop_names:
|
||||
layout.prop(door_gizmos, prop)
|
||||
|
||||
@@ -846,7 +864,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
|
||||
window_gizmos = self.gizmos.window
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoWindowEdition.dimension_gizmo_props}
|
||||
for prop in window_gizmos.__annotations__:
|
||||
try:
|
||||
annotations = window_gizmos.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(window_gizmos).__annotations__
|
||||
for prop in annotations:
|
||||
if prop in gizmo_prop_names:
|
||||
layout.prop(window_gizmos, prop)
|
||||
|
||||
@@ -857,7 +879,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoStairEdition.dimension_gizmo_props}
|
||||
# Add special gizmos not in dimension_gizmo_props
|
||||
special_gizmo_names = {"lock", "plus", "minus", "cycle"}
|
||||
for prop in stair_gizmos.__annotations__:
|
||||
try:
|
||||
annotations = stair_gizmos.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(stair_gizmos).__annotations__
|
||||
for prop in annotations:
|
||||
if prop in gizmo_prop_names or prop in special_gizmo_names:
|
||||
layout.prop(stair_gizmos, prop)
|
||||
|
||||
@@ -966,9 +992,26 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
row = layout.row()
|
||||
row.separator()
|
||||
row.prop(self, "metadata_blend_file_suffix")
|
||||
row = layout.row()
|
||||
row.separator()
|
||||
row.prop(self, "user_ui_customization")
|
||||
|
||||
bprops = tool.Blender.get_bim_props()
|
||||
if tab_visibilities := bprops.tab_visibilities:
|
||||
row = layout.row()
|
||||
row.operator("bim.reset_ui_layout", icon="LOOP_BACK")
|
||||
row = layout.row(align=True)
|
||||
row.template_list(
|
||||
"BIM_UL_tab_visibilities", "", bprops, "tab_visibilities", bprops, "active_tab_visibility_index"
|
||||
)
|
||||
row.template_list(
|
||||
"BIM_UL_panel_visibilities",
|
||||
"",
|
||||
bprops,
|
||||
"panel_visibilities",
|
||||
bprops,
|
||||
"active_panel_visibility_index",
|
||||
)
|
||||
else:
|
||||
row = layout.row()
|
||||
row.operator("bim.manage_tab_visibility", icon="PREFERENCES")
|
||||
|
||||
|
||||
# Scene panel groups
|
||||
@@ -984,78 +1027,31 @@ class BIM_PT_tabs(Panel):
|
||||
def draw(self, context):
|
||||
if not UIData.is_loaded:
|
||||
UIData.load()
|
||||
is_ifc_project = bool(tool.Ifc.get())
|
||||
aprops = tool.Blender.get_area_props(context)
|
||||
addon_prefs = tool.Blender.get_addon_preferences()
|
||||
ifc_icon = f"{UIData.data['tabs_icon_color_mode']}_ifc"
|
||||
|
||||
split = self.layout.split(factor=0.9)
|
||||
col_left = split.column(align=True)
|
||||
row_left = col_left.row(align=True)
|
||||
row_left.alignment = "CENTER"
|
||||
if get_tab_visibility("PROJECT"):
|
||||
row_left.operator(
|
||||
"bim.set_tab",
|
||||
text="",
|
||||
emboss=aprops.tab == "PROJECT",
|
||||
icon_value=bonsai.bim.icons[ifc_icon].icon_id,
|
||||
).tab = "PROJECT"
|
||||
if get_tab_visibility("OBJECT"):
|
||||
self.draw_tab_entry(row_left, "FILE_3D", "OBJECT", is_ifc_project, aprops.tab == "OBJECT")
|
||||
if get_tab_visibility("GEOMETRY"):
|
||||
self.draw_tab_entry(row_left, "MATERIAL", "GEOMETRY", is_ifc_project, aprops.tab == "GEOMETRY")
|
||||
if get_tab_visibility("DRAWINGS"):
|
||||
self.draw_tab_entry(row_left, "DOCUMENTS", "DRAWINGS", is_ifc_project, aprops.tab == "DRAWINGS")
|
||||
if get_tab_visibility("SERVICES"):
|
||||
self.draw_tab_entry(row_left, "NETWORK_DRIVE", "SERVICES", is_ifc_project, aprops.tab == "SERVICES")
|
||||
if get_tab_visibility("STRUCTURE"):
|
||||
self.draw_tab_entry(row_left, "EDITMODE_HLT", "STRUCTURE", is_ifc_project, aprops.tab == "STRUCTURE")
|
||||
if get_tab_visibility("SCHEDULING"):
|
||||
self.draw_tab_entry(row_left, "NLA", "SCHEDULING", is_ifc_project, aprops.tab == "SCHEDULING")
|
||||
if get_tab_visibility("FM"):
|
||||
self.draw_tab_entry(row_left, "PACKAGE", "FM", True, aprops.tab == "FM")
|
||||
if get_tab_visibility("QUALITY"):
|
||||
self.draw_tab_entry(row_left, "COMMUNITY", "QUALITY", True, aprops.tab == "QUALITY")
|
||||
if (
|
||||
addon_prefs.save_metadata_blend_file
|
||||
and addon_prefs.user_ui_customization
|
||||
and get_tab_visibility("BOOKMARK")
|
||||
):
|
||||
self.draw_tab_entry(row_left, "SOLO_ON", "BOOKMARK", True, aprops.tab == "BOOKMARK")
|
||||
row_left.operator("bim.switch_tab", text="", emboss=False, icon="UV_SYNC_SELECT")
|
||||
row = self.layout.row()
|
||||
row.alignment = "CENTER"
|
||||
for tab in UIData.data["tabs"]:
|
||||
self.draw_tab_entry(row, tab[1], tab[0], tab[2], aprops.tab == tab[0])
|
||||
row.operator("bim.switch_tab", text="", emboss=False, icon="UV_SYNC_SELECT")
|
||||
|
||||
row_left = col_left.row(align=True)
|
||||
row = self.layout.row()
|
||||
# Yes, that's right.
|
||||
row_left.alignment = "CENTER"
|
||||
row_left.scale_y = 0.2
|
||||
row.alignment = "CENTER"
|
||||
row.scale_y = 0.2
|
||||
|
||||
if not (addon_prefs.save_metadata_blend_file and addon_prefs.user_ui_customization):
|
||||
row_left.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False)
|
||||
|
||||
for tab in get_tab_names():
|
||||
for tab in UIData.data["tabs"]:
|
||||
# Draw a little underscore below the active tab icon.
|
||||
if get_tab_visibility(tab):
|
||||
if aprops.tab == tab:
|
||||
row_left.prop(aprops, "active_tab", text="", icon="BLANK1")
|
||||
else:
|
||||
row_left.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False)
|
||||
row_left.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False) # space for Switch
|
||||
col_right = split.column(align=True)
|
||||
row_right = col_right.row(align=True)
|
||||
row_right.alignment = "RIGHT"
|
||||
if aprops.tab == tab[0]:
|
||||
row.prop(aprops, "active_tab", text="", icon="BLANK1")
|
||||
else:
|
||||
row.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False)
|
||||
row.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False) # space for Switch
|
||||
|
||||
if addon_prefs.save_metadata_blend_file and addon_prefs.user_ui_customization:
|
||||
row_right.operator("bim.manage_tab_visibility", icon="PREFERENCES", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row = self.layout.row()
|
||||
row.prop(aprops, "tab", text="")
|
||||
|
||||
if addon_prefs.save_metadata_blend_file and addon_prefs.user_ui_customization:
|
||||
for tab in get_tab_names():
|
||||
if get_tab_visibility(tab):
|
||||
if aprops.tab == tab:
|
||||
row.operator("bim.manage_tab_panels", text="", icon="PREFERENCES").tab_name = tab
|
||||
|
||||
if bonsai.REINSTALLED_BBIM_VERSION:
|
||||
box = self.layout.box()
|
||||
box.alert = True
|
||||
@@ -1121,7 +1117,10 @@ class BIM_PT_tabs(Panel):
|
||||
|
||||
def draw_tab_entry(self, row, icon, tab_name, enabled=True, highlight=True):
|
||||
tab_entry = row.row(align=True)
|
||||
tab_entry.operator("bim.set_tab", text="", emboss=highlight, icon=icon).tab = tab_name
|
||||
if isinstance(icon, int):
|
||||
tab_entry.operator("bim.set_tab", text="", emboss=highlight, icon_value=icon).tab = tab_name
|
||||
else:
|
||||
tab_entry.operator("bim.set_tab", text="", emboss=highlight, icon=icon).tab = tab_name
|
||||
tab_entry.enabled = enabled
|
||||
|
||||
|
||||
@@ -1135,9 +1134,7 @@ class BIM_PT_tab_new_project_wizard(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if not tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if not tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return False
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
pprops = tool.Project.get_project_props()
|
||||
@@ -1161,20 +1158,16 @@ class BIM_PT_tab_project_info(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
pprops = tool.Project.get_project_props()
|
||||
if pprops.is_loading:
|
||||
return True
|
||||
elif tool.Ifc.get() or bim_props.ifc_file:
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="This is the Project Info panel.")
|
||||
pass
|
||||
|
||||
|
||||
class BIM_PT_tab_spatial(Panel):
|
||||
@@ -1187,11 +1180,8 @@ class BIM_PT_tab_spatial(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1207,11 +1197,8 @@ class BIM_PT_tab_project_setup(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1228,11 +1215,8 @@ class BIM_PT_tab_stakeholders(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1248,11 +1232,8 @@ class BIM_PT_tab_collaboration(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1269,11 +1250,8 @@ class BIM_PT_tab_grouping_and_filtering(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1297,11 +1275,8 @@ class BIM_PT_tab_geometry(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1317,11 +1292,8 @@ class BIM_PT_tab_status(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1337,11 +1309,8 @@ class BIM_PT_tab_qto(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1357,11 +1326,8 @@ class BIM_PT_tab_resources(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1377,11 +1343,8 @@ class BIM_PT_tab_cost(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1397,11 +1360,8 @@ class BIM_PT_tab_sequence(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1417,11 +1377,8 @@ class BIM_PT_tab_structural(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1437,11 +1394,8 @@ class BIM_PT_tab_services(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1457,11 +1411,8 @@ class BIM_PT_tab_lighting(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1477,11 +1428,8 @@ class BIM_PT_tab_zones(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1497,11 +1445,8 @@ class BIM_PT_tab_solar_analysis(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1517,11 +1462,8 @@ class BIM_PT_tab_quality_control(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1537,11 +1479,8 @@ class BIM_PT_tab_clash_detection(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1558,11 +1497,8 @@ class BIM_PT_tab_sandbox(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row()
|
||||
@@ -1581,11 +1517,9 @@ class BIM_PT_tab_object_metadata(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
props = tool.Project.get_project_props()
|
||||
if (
|
||||
tool.Blender.is_tab(context, cls.bim_tab_name)
|
||||
tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname)
|
||||
and tool.Ifc.get()
|
||||
and (obj := context.active_object)
|
||||
# Hide links empty handles.
|
||||
@@ -1596,7 +1530,6 @@ class BIM_PT_tab_object_metadata(Panel):
|
||||
)
|
||||
):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1613,16 +1546,13 @@ class BIM_PT_tab_placement(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if (
|
||||
tool.Blender.is_tab(context, cls.bim_tab_name)
|
||||
tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname)
|
||||
and tool.Ifc.get()
|
||||
and (obj := context.active_object)
|
||||
and tool.Ifc.get_entity(obj)
|
||||
):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1639,11 +1569,8 @@ class BIM_PT_tab_representations(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1661,11 +1588,8 @@ class BIM_PT_tab_geometric_relationships(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1683,11 +1607,8 @@ class BIM_PT_tab_parametric_geometry(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1704,11 +1625,8 @@ class BIM_PT_tab_object_materials(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1725,11 +1643,8 @@ class BIM_PT_tab_materials(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1746,11 +1661,8 @@ class BIM_PT_tab_styles(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1767,11 +1679,8 @@ class BIM_PT_tab_profiles(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1788,11 +1697,8 @@ class BIM_PT_tab_sheets(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1809,11 +1715,8 @@ class BIM_PT_tab_drawings(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1830,11 +1733,8 @@ class BIM_PT_tab_schedules(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1851,11 +1751,8 @@ class BIM_PT_tab_references(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1873,11 +1770,8 @@ class BIM_PT_tab_misc(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get():
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname) and tool.Ifc.get():
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1894,11 +1788,8 @@ class BIM_PT_tab_handover(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1915,11 +1806,8 @@ class BIM_PT_tab_operations(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context):
|
||||
return False
|
||||
if tool.Blender.is_tab(context, cls.bim_tab_name):
|
||||
if tool.Blender.should_show_panel(context, cls.bim_tab_name, cls.bl_idname):
|
||||
return True
|
||||
return tool.Blender.is_tab(context, "BOOKMARK")
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
@@ -1960,8 +1848,8 @@ class UIData:
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"version": cls.version(),
|
||||
"tabs_icon_color_mode": cls.icon_color_mode("user_interface.wcol_regular.text"),
|
||||
"menu_icon_color_mode": cls.icon_color_mode("user_interface.wcol_menu.text"),
|
||||
"tabs": cls.tabs(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@@ -1973,6 +1861,28 @@ class UIData:
|
||||
def icon_color_mode(cls, color_path):
|
||||
return tool.Blender.detect_icon_color_mode(color_path)
|
||||
|
||||
@classmethod
|
||||
def tabs(cls):
|
||||
hidden_tabs = [t.name for t in tool.Blender.get_bim_props().tab_visibilities if not t.is_visible]
|
||||
color_mode = cls.icon_color_mode("user_interface.wcol_regular.text")
|
||||
is_ifc_project = bool(tool.Ifc.get())
|
||||
return [
|
||||
tab
|
||||
for tab in [
|
||||
("PROJECT", bonsai.bim.icons[f"{color_mode}_ifc"].icon_id, True),
|
||||
("OBJECT", "FILE_3D", is_ifc_project),
|
||||
("GEOMETRY", "MATERIAL", is_ifc_project),
|
||||
("DRAWINGS", "DOCUMENTS", is_ifc_project),
|
||||
("SERVICES", "NETWORK_DRIVE", is_ifc_project),
|
||||
("STRUCTURE", "EDITMODE_HLT", is_ifc_project),
|
||||
("SCHEDULING", "NLA", is_ifc_project),
|
||||
("FM", "PACKAGE", True),
|
||||
("QUALITY", "COMMUNITY", True),
|
||||
("BOOKMARK", "SOLO_ON", is_ifc_project),
|
||||
]
|
||||
if tab[0] not in hidden_tabs
|
||||
]
|
||||
|
||||
|
||||
def draw_statusbar(self, context):
|
||||
if not UIData.is_loaded:
|
||||
|
||||
@@ -20,7 +20,6 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
|
||||
@@ -28,102 +27,82 @@ if TYPE_CHECKING:
|
||||
def load_project_documents(document: tool.Document) -> None:
|
||||
document.clear_document_tree()
|
||||
document.import_project_documents()
|
||||
document.clear_breadcrumbs()
|
||||
document.enable_editing_ui()
|
||||
|
||||
|
||||
def load_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None:
|
||||
document_tool.clear_document_tree()
|
||||
document_tool.import_subdocuments(document)
|
||||
document_tool.import_references(document)
|
||||
document_tool.disable_editing_document()
|
||||
document_tool.add_breadcrumb(document)
|
||||
|
||||
|
||||
def load_parent_document(document: tool.Document) -> None:
|
||||
document.clear_document_tree()
|
||||
document.remove_latest_breadcrumb()
|
||||
parent = document.get_active_breadcrumb()
|
||||
if parent:
|
||||
document.import_subdocuments(parent)
|
||||
document.import_references(parent)
|
||||
document.disable_editing_document()
|
||||
else:
|
||||
document.import_project_documents()
|
||||
|
||||
|
||||
def disable_document_editing_ui(document: tool.Document) -> None:
|
||||
document.disable_editing_ui()
|
||||
document.disable_editing_document()
|
||||
|
||||
|
||||
def enable_editing_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None:
|
||||
document_tool.import_document_attributes(document)
|
||||
document_tool.set_active_document(document)
|
||||
def disable_object_document_editing_ui(document: tool.Document) -> None:
|
||||
document.disable_object_editing_ui()
|
||||
|
||||
|
||||
def enable_editing_document(document: tool.Document, ifc_document: ifcopenshell.entity_instance) -> None:
|
||||
document.set_active_document(ifc_document)
|
||||
document.import_document_attributes(ifc_document)
|
||||
|
||||
|
||||
def disable_editing_document(document: tool.Document) -> None:
|
||||
document.disable_editing_document()
|
||||
document.clear_active_document()
|
||||
document.clear_document_attributes()
|
||||
|
||||
|
||||
def add_information(ifc: tool.Ifc, document: tool.Document) -> None:
|
||||
def add_information(ifc: tool.Ifc, document: tool.Document, parent=None) -> ifcopenshell.entity_instance:
|
||||
document.clear_document_tree()
|
||||
parent = document.get_active_breadcrumb()
|
||||
|
||||
if parent is None:
|
||||
parent = document.get_default_parent_for_information()
|
||||
|
||||
information = ifc.run("document.add_information", parent=parent)
|
||||
ifc.run("document.add_reference", information=information)
|
||||
if parent:
|
||||
document.import_subdocuments(parent)
|
||||
document.import_references(parent)
|
||||
else:
|
||||
document.import_project_documents()
|
||||
|
||||
if document.is_document_information(parent):
|
||||
document.expand_document(parent)
|
||||
|
||||
document.import_project_documents()
|
||||
return information
|
||||
|
||||
|
||||
def add_reference(ifc: tool.Ifc, document: tool.Document) -> None:
|
||||
parent = document.get_active_breadcrumb()
|
||||
assert parent
|
||||
ifc.run("document.add_reference", information=parent)
|
||||
parent = document.get_selected_document_information()
|
||||
|
||||
if parent:
|
||||
reference = ifc.run("document.add_reference", information=parent)
|
||||
reference.Location = ""
|
||||
document.expand_document(parent)
|
||||
|
||||
document.import_project_documents()
|
||||
|
||||
|
||||
def edit_document(ifc: tool.Ifc, document: tool.Document, ifc_document: ifcopenshell.entity_instance) -> None:
|
||||
attributes = document.export_document_attributes()
|
||||
if document.is_document_information(ifc_document):
|
||||
ifc.run("document.edit_information", information=ifc_document, attributes=attributes)
|
||||
else:
|
||||
ifc.run("document.edit_reference", reference=ifc_document, attributes=attributes)
|
||||
document.disable_editing_document()
|
||||
document.clear_document_tree()
|
||||
document.import_subdocuments(parent)
|
||||
document.import_references(parent)
|
||||
document.import_project_documents()
|
||||
|
||||
|
||||
def edit_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None:
|
||||
attributes = document_tool.export_document_attributes()
|
||||
if document_tool.is_document_information(document):
|
||||
ifc.run("document.edit_information", information=document, attributes=attributes)
|
||||
def remove_document(ifc: tool.Ifc, document: tool.Document, ifc_document: ifcopenshell.entity_instance) -> None:
|
||||
document.clear_document_tree()
|
||||
if document.is_document_information(ifc_document):
|
||||
ifc.run("document.remove_information", information=ifc_document)
|
||||
else:
|
||||
ifc.run("document.edit_reference", reference=document, attributes=attributes)
|
||||
document_tool.disable_editing_document()
|
||||
document_tool.clear_document_tree()
|
||||
parent = document_tool.get_active_breadcrumb()
|
||||
if parent:
|
||||
document_tool.import_subdocuments(parent)
|
||||
document_tool.import_references(parent)
|
||||
else:
|
||||
document_tool.import_project_documents()
|
||||
|
||||
|
||||
def remove_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None:
|
||||
document_tool.clear_document_tree()
|
||||
if document_tool.is_document_information(document):
|
||||
ifc.run("document.remove_information", information=document)
|
||||
else:
|
||||
ifc.run("document.remove_reference", reference=document)
|
||||
parent = document_tool.get_active_breadcrumb()
|
||||
if parent:
|
||||
document_tool.import_subdocuments(parent)
|
||||
document_tool.import_references(parent)
|
||||
else:
|
||||
document_tool.import_project_documents()
|
||||
ifc.run("document.remove_reference", reference=ifc_document)
|
||||
document.import_project_documents()
|
||||
|
||||
|
||||
def assign_document(
|
||||
ifc: tool.Ifc, product: ifcopenshell.entity_instance, document: ifcopenshell.entity_instance
|
||||
ifc: tool.Ifc, product: ifcopenshell.entity_instance, ifc_document: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
ifc.run("document.assign_document", products=[product], document=document)
|
||||
ifc.run("document.assign_document", products=[product], document=ifc_document)
|
||||
|
||||
|
||||
def unassign_document(
|
||||
ifc: tool.Ifc, product: ifcopenshell.entity_instance, document: ifcopenshell.entity_instance
|
||||
ifc: tool.Ifc, product: ifcopenshell.entity_instance, ifc_document: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
ifc.run("document.unassign_document", products=[product], document=document)
|
||||
ifc.run("document.unassign_document", products=[product], document=ifc_document)
|
||||
|
||||
@@ -402,7 +402,7 @@ def update_drawing_name(
|
||||
camera = ifc.get_object(drawing)
|
||||
if camera and camera.name != name:
|
||||
camera.name = name
|
||||
|
||||
|
||||
group = drawing_tool.get_drawing_group(drawing)
|
||||
if drawing_tool.get_name(group) != name:
|
||||
ifc.run("attribute.edit_attributes", product=group, attributes={"Name": name})
|
||||
|
||||
@@ -63,20 +63,20 @@ def copy_class(
|
||||
|
||||
def _has_material_styles(ifc: type[tool.Ifc], element: ifcopenshell.entity_instance) -> bool:
|
||||
"""Check if element has styles defined through its material.
|
||||
|
||||
|
||||
Returns True if any constituent material has a style representation,
|
||||
which means styles should NOT be applied directly to the geometry.
|
||||
"""
|
||||
materials = ifcopenshell.util.element.get_materials(element)
|
||||
|
||||
|
||||
if not materials:
|
||||
return False
|
||||
|
||||
|
||||
# Check if any of the constituent materials have styles
|
||||
for material in materials:
|
||||
if hasattr(material, 'HasRepresentation') and material.HasRepresentation:
|
||||
if hasattr(material, "HasRepresentation") and material.HasRepresentation:
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -287,21 +287,29 @@ class Debug:
|
||||
|
||||
@interface
|
||||
class Document:
|
||||
def add_breadcrumb(cls, document): pass
|
||||
def clear_breadcrumbs(cls): pass
|
||||
def clear_document_tree(cls): pass
|
||||
def disable_editing_document(cls): pass
|
||||
def disable_object_editing_ui(cls): pass
|
||||
def disable_editing_ui(cls): pass
|
||||
def enable_editing_ui(cls): pass
|
||||
def export_document_attributes(cls): pass
|
||||
def get_active_breadcrumb(cls): pass
|
||||
def import_document_attributes(cls, document): pass
|
||||
def import_project_documents(cls): pass
|
||||
def import_references(cls, document): pass
|
||||
def import_subdocuments(cls, document): pass
|
||||
def is_document_information(cls, document): pass
|
||||
def remove_latest_breadcrumb(cls): pass
|
||||
def set_active_document(cls, document): pass
|
||||
def clear_active_document(cls): pass
|
||||
def clear_document_attributes(cls): pass
|
||||
def expand_document(cls, document): pass
|
||||
def get_default_parent_for_information(cls): pass
|
||||
def get_selected_document_information(cls): pass
|
||||
def get_document_information_id(cls, document): pass
|
||||
def set_document_information_id(cls, document, value): pass
|
||||
def get_external_reference_id(cls, reference): pass
|
||||
def set_external_reference_id(cls, reference, value): pass
|
||||
def get_document_references(cls, document): pass
|
||||
def refresh_document_data(cls): pass
|
||||
def load_document_objects_into_props(cls, document_id): pass
|
||||
def update_document_objects(cls, document_id): pass
|
||||
|
||||
|
||||
@interface
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
from __future__ import annotations
|
||||
import bonsai.core.geometry
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import ifcopenshell.util.element
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
@@ -32,14 +33,34 @@ def assign_type(
|
||||
element: ifcopenshell.entity_instance,
|
||||
type: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
|
||||
# Get the instance's current CardinalPoint before type assignment
|
||||
instance_cardinal_point = None
|
||||
instance_material = ifcopenshell.util.element.get_material(element)
|
||||
if instance_material and instance_material.is_a("IfcMaterialProfileSetUsage"):
|
||||
instance_cardinal_point = instance_material.CardinalPoint
|
||||
|
||||
ifc.run("type.assign_type", related_objects=[element], relating_type=type)
|
||||
obj = ifc.get_object(element)
|
||||
|
||||
if type_tool.has_material_usage(element):
|
||||
pass # for now, representation regeneration handled by API listeners
|
||||
# Restore the instance's CardinalPoint to the new material usage
|
||||
if instance_cardinal_point is not None:
|
||||
new_instance_material = ifcopenshell.util.element.get_material(element)
|
||||
if new_instance_material and new_instance_material.is_a("IfcMaterialProfileSetUsage"):
|
||||
if new_instance_material.CardinalPoint != instance_cardinal_point:
|
||||
new_instance_material.CardinalPoint = instance_cardinal_point
|
||||
|
||||
# Force representation regeneration
|
||||
from bonsai.bim.module.model.profile import DumbProfileRecalculator
|
||||
|
||||
DumbProfileRecalculator().recalculate([obj])
|
||||
# for now, representation regeneration handled by API listeners
|
||||
else:
|
||||
type_data = type_tool.get_object_data(ifc.get_object(type))
|
||||
if type_data:
|
||||
type_tool.change_object_data(obj, type_data, is_global=False)
|
||||
|
||||
type_tool.disable_editing(obj)
|
||||
|
||||
|
||||
|
||||
@@ -66,8 +66,6 @@ def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None:
|
||||
else:
|
||||
timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower())
|
||||
units += [massunit, timeunit]
|
||||
print("Add mass and time units:", unit.add_mass_and_time_units())
|
||||
print("Assigning units:", units)
|
||||
ifc.run("unit.assign_unit", units=units)
|
||||
|
||||
|
||||
|
||||
@@ -161,11 +161,19 @@ class Blender(bonsai.core.tool.Blender):
|
||||
screen.BIMAreaProperties.add()
|
||||
|
||||
@classmethod
|
||||
def is_tab(cls, context: bpy.types.Context, tab: str) -> bool:
|
||||
def should_show_panel(cls, context: bpy.types.Context, tab: str, panel: str) -> bool:
|
||||
aprops = cls.get_area_props(context)
|
||||
if aprops.path_from_id() == "BIMAreaProperties" and context.area.spaces.active.search_filter:
|
||||
return True
|
||||
return aprops.tab == tab
|
||||
if (is_bookmark_tab := aprops.tab == "BOOKMARK") or aprops.tab == tab:
|
||||
bprops = tool.Blender.get_bim_props()
|
||||
if not (panel_visibility := bprops.panel_visibilities.get(panel)):
|
||||
return not is_bookmark_tab
|
||||
if is_bookmark_tab:
|
||||
if panel_visibility.is_bookmarked:
|
||||
return True
|
||||
elif panel_visibility.is_visible:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_default_scene(cls) -> bool:
|
||||
@@ -382,17 +390,14 @@ class Blender(bonsai.core.tool.Blender):
|
||||
assert isinstance(space, bpy.types.SpaceNodeEditor)
|
||||
if space.tree_type == "ShaderNodeTree":
|
||||
context_override = {"area": area, "space": space, "screen": screen}
|
||||
|
||||
|
||||
# Add window if screen differs from current context
|
||||
context = bpy.context
|
||||
if context and context.screen != screen:
|
||||
window = next(
|
||||
(w for w in context.window_manager.windows if w.screen == screen),
|
||||
None
|
||||
)
|
||||
window = next((w for w in context.window_manager.windows if w.screen == screen), None)
|
||||
if window:
|
||||
context_override["window"] = window
|
||||
|
||||
|
||||
return context_override
|
||||
|
||||
@classmethod
|
||||
@@ -742,7 +747,11 @@ class Blender(bonsai.core.tool.Blender):
|
||||
# Yes, accessing items through annotations is a bit hacky
|
||||
# but it's the only way to get the dynamic enum items
|
||||
# besides providing them to get_enum_safe explicitly.
|
||||
prop_keywords = props.__annotations__[prop_name].keywords
|
||||
try:
|
||||
annotations = props.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(props).__annotations__
|
||||
prop_keywords = annotations[prop_name].keywords
|
||||
items = prop_keywords.get("items")
|
||||
if items is None:
|
||||
return None
|
||||
@@ -1460,7 +1469,10 @@ class Blender(bonsai.core.tool.Blender):
|
||||
def override_scene_panel(cls, original_panel: bpy.types.Panel) -> None:
|
||||
@classmethod
|
||||
def poll_check_blender_tab(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
aprops = tool.Blender.get_area_props(context)
|
||||
if aprops.path_from_id() == "BIMAreaProperties" and context.area.spaces.active.search_filter:
|
||||
return True
|
||||
return aprops.tab == "BLENDER"
|
||||
|
||||
polls = bonsai.bim.original_scene_panels_polls
|
||||
|
||||
|
||||
@@ -44,8 +44,6 @@ class Collector(bonsai.core.tool.Collector):
|
||||
# Note that tool.Geometry.is_locked is only checked within the if
|
||||
# statements for efficiency as it is a slow check.
|
||||
tool.Geometry.lock_scale(obj)
|
||||
if element.is_a("IfcSlab"):
|
||||
tool.Geometry.lock_rotation(obj, x=True)
|
||||
|
||||
if element.is_a("IfcGridAxis"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
|
||||
@@ -22,6 +22,8 @@ import ifcopenshell.util.system
|
||||
import bonsai.bim.helper
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import json
|
||||
from natsort import natsorted
|
||||
from typing import Any, Union, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -33,17 +35,6 @@ class Document(bonsai.core.tool.Document):
|
||||
def get_document_props(cls) -> BIMDocumentProperties:
|
||||
return bpy.context.scene.BIMDocumentProperties
|
||||
|
||||
@classmethod
|
||||
def add_breadcrumb(cls, document: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_document_props()
|
||||
new = props.breadcrumbs.add()
|
||||
new.name = str(document.id())
|
||||
|
||||
@classmethod
|
||||
def clear_breadcrumbs(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
props.breadcrumbs.clear()
|
||||
|
||||
@classmethod
|
||||
def clear_document_tree(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
@@ -54,6 +45,11 @@ class Document(bonsai.core.tool.Document):
|
||||
props = cls.get_document_props()
|
||||
props.active_document_id = 0
|
||||
|
||||
@classmethod
|
||||
def disable_object_editing_ui(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
props.is_object_editing = False
|
||||
|
||||
@classmethod
|
||||
def disable_editing_ui(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
@@ -69,18 +65,15 @@ class Document(bonsai.core.tool.Document):
|
||||
props = cls.get_document_props()
|
||||
return bonsai.bim.helper.export_attributes(props.document_attributes)
|
||||
|
||||
@classmethod
|
||||
def get_active_breadcrumb(cls) -> Union[ifcopenshell.entity_instance, None]:
|
||||
props = cls.get_document_props()
|
||||
if len(props.breadcrumbs):
|
||||
return tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
|
||||
|
||||
@classmethod
|
||||
def import_document_attributes(cls, document: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_document_props()
|
||||
props.document_attributes.clear()
|
||||
|
||||
def callback(attr_name: str, _, data: dict[str, Any]) -> Union[bool, None]:
|
||||
def callback(attr_name: str, attr_value: Any, data: dict[str, Any]) -> Union[bool, None]:
|
||||
if attr_name == "Location" and attr_value is None:
|
||||
data[attr_name] = ""
|
||||
return True
|
||||
if attr_name != "Name":
|
||||
return None # Proceed normally
|
||||
|
||||
@@ -100,52 +93,132 @@ class Document(bonsai.core.tool.Document):
|
||||
def import_project_documents(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
props.documents.clear()
|
||||
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
file = tool.Ifc.get()
|
||||
try:
|
||||
expanded_documents = json.loads(props.json_string)
|
||||
except (AttributeError, json.JSONDecodeError):
|
||||
expanded_documents = []
|
||||
|
||||
project = file.by_type("IfcProject")[0] if file.by_type("IfcProject") else None
|
||||
if not project:
|
||||
return
|
||||
|
||||
document_children = {}
|
||||
|
||||
for rel in file.by_type("IfcDocumentInformationRelationship"):
|
||||
parent_id = rel.RelatingDocument.id()
|
||||
if parent_id not in document_children:
|
||||
document_children[parent_id] = []
|
||||
|
||||
for child in rel.RelatedDocuments:
|
||||
document_children[parent_id].append(child)
|
||||
|
||||
is_ifc2x3 = file.schema == "IFC2X3"
|
||||
|
||||
if is_ifc2x3:
|
||||
for ref in file.by_type("IfcDocumentReference"):
|
||||
if ref.ReferenceToDocument:
|
||||
parent = ref.ReferenceToDocument[0]
|
||||
parent_id = parent.id()
|
||||
if parent_id not in document_children:
|
||||
document_children[parent_id] = []
|
||||
document_children[parent_id].append(ref)
|
||||
else:
|
||||
for ref in file.by_type("IfcDocumentReference"):
|
||||
if ref.ReferencedDocument:
|
||||
parent = ref.ReferencedDocument
|
||||
parent_id = parent.id()
|
||||
if parent_id not in document_children:
|
||||
document_children[parent_id] = []
|
||||
document_children[parent_id].append(ref)
|
||||
|
||||
root_documents = []
|
||||
for rel in project.HasAssociations or []:
|
||||
if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation"):
|
||||
element = rel.RelatingDocument
|
||||
new = props.documents.add()
|
||||
new.ifc_definition_id = element.id()
|
||||
new["name"] = element.Name or "Unnamed"
|
||||
new.is_information = True
|
||||
new["identification"] = cls.get_document_information_id(element)
|
||||
is_child = False
|
||||
for children in document_children.values():
|
||||
if rel.RelatingDocument in children:
|
||||
is_child = True
|
||||
break
|
||||
|
||||
if not is_child:
|
||||
root_documents.append(rel.RelatingDocument)
|
||||
|
||||
root = props.documents.add()
|
||||
root.ifc_definition_id = -project.id()
|
||||
root.document_type = "PROJECT"
|
||||
root.name = f"Project Documents ({project.Name or 'Unnamed Project'})"
|
||||
root.identification = ""
|
||||
root.location = ""
|
||||
root.tree_depth = 0
|
||||
root.has_children = bool(root_documents)
|
||||
|
||||
root_id = -project.id()
|
||||
|
||||
root.is_expanded = root_id not in expanded_documents
|
||||
|
||||
if root.is_expanded:
|
||||
root_documents = natsorted(
|
||||
root_documents, key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "")
|
||||
)
|
||||
|
||||
for doc in root_documents:
|
||||
cls._process_document(doc, props, document_children, expanded_documents, 1)
|
||||
|
||||
@classmethod
|
||||
def import_references(cls, document: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_document_props()
|
||||
is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3"
|
||||
references = cls.get_document_references(document)
|
||||
for element in references:
|
||||
new = props.documents.add()
|
||||
new.ifc_definition_id = element.id()
|
||||
# Use Description + Location instead of Name as IFC has a restriction
|
||||
# for IfcDocumentReference to have Name only if it has no ReferencedDocument.
|
||||
name = " - ".join([x for x in [element.Description, element.Location] if x])
|
||||
new["name"] = name or "Unnamed"
|
||||
new["identification"] = cls.get_external_reference_id(element)
|
||||
new.is_information = False
|
||||
def _process_document(cls, document, props, document_children, expanded_documents, depth):
|
||||
new = props.documents.add()
|
||||
new.ifc_definition_id = document.id()
|
||||
new.document_type = "INFORMATION" if document.is_a("IfcDocumentInformation") else "REFERENCE"
|
||||
new.tree_depth = depth
|
||||
|
||||
@classmethod
|
||||
def import_subdocuments(cls, document: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_document_props()
|
||||
if document.IsPointer:
|
||||
for element in document.IsPointer[0].RelatedDocuments or []:
|
||||
new = props.documents.add()
|
||||
new.ifc_definition_id = element.id()
|
||||
new["name"] = element.Name or "Unnamed"
|
||||
new.is_information = True
|
||||
new["identification"] = cls.get_document_information_id(element) or "*"
|
||||
new.name = document.Name or ""
|
||||
new.identification = (
|
||||
cls.get_document_information_id(document)
|
||||
if new.document_type == "INFORMATION"
|
||||
else cls.get_external_reference_id(document)
|
||||
)
|
||||
new.identification = new.identification or ""
|
||||
new.description = document.Description or ""
|
||||
new.location = document.Location or ""
|
||||
|
||||
if new.document_type == "INFORMATION":
|
||||
new.name = document.Name or "Unnamed"
|
||||
|
||||
elif new.document_type == "REFERENCE":
|
||||
file = document.file
|
||||
if file.schema == "IFC2X3":
|
||||
if document.ReferenceToDocument and not new.name:
|
||||
new.name = document.ReferenceToDocument[0].Name or ""
|
||||
else:
|
||||
if document.ReferencedDocument and not new.name:
|
||||
new.name = document.ReferencedDocument.Name or ""
|
||||
|
||||
doc_id = document.id()
|
||||
has_children = doc_id in document_children and bool(document_children[doc_id])
|
||||
new.has_children = has_children
|
||||
new.is_expanded = doc_id in expanded_documents
|
||||
|
||||
if has_children and new.is_expanded:
|
||||
children = document_children[doc_id]
|
||||
|
||||
info_children = natsorted(
|
||||
[d for d in children if d.is_a("IfcDocumentInformation")],
|
||||
key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or ""),
|
||||
)
|
||||
|
||||
ref_children = natsorted(
|
||||
[d for d in children if not d.is_a("IfcDocumentInformation")],
|
||||
key=lambda doc: (cls.get_external_reference_id(doc) or "", doc.Description or doc.Name or ""),
|
||||
)
|
||||
|
||||
for child in info_children + ref_children:
|
||||
cls._process_document(child, props, document_children, expanded_documents, depth + 1)
|
||||
|
||||
@classmethod
|
||||
def is_document_information(cls, document: ifcopenshell.entity_instance) -> bool:
|
||||
return document.is_a("IfcDocumentInformation")
|
||||
|
||||
@classmethod
|
||||
def remove_latest_breadcrumb(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
if len(props.breadcrumbs):
|
||||
props.breadcrumbs.remove(len(props.breadcrumbs) - 1)
|
||||
|
||||
@classmethod
|
||||
def set_active_document(cls, document: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_document_props()
|
||||
@@ -179,3 +252,65 @@ class Document(bonsai.core.tool.Document):
|
||||
if document.file.schema == "IFC2X3":
|
||||
return document.DocumentReferences or ()
|
||||
return document.HasDocumentReferences
|
||||
|
||||
@classmethod
|
||||
def clear_active_document(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
props.active_document_id = 0
|
||||
|
||||
@classmethod
|
||||
def clear_document_attributes(cls) -> None:
|
||||
props = cls.get_document_props()
|
||||
props.document_attributes.clear()
|
||||
|
||||
@classmethod
|
||||
def expand_document(cls, document: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_document_props()
|
||||
try:
|
||||
expanded_docs = json.loads(props.json_string)
|
||||
except (AttributeError, json.JSONDecodeError):
|
||||
expanded_docs = []
|
||||
|
||||
if document.id() not in expanded_docs:
|
||||
expanded_docs.append(document.id())
|
||||
props.json_string = json.dumps(expanded_docs)
|
||||
|
||||
@classmethod
|
||||
def get_default_parent_for_information(cls) -> Union[ifcopenshell.entity_instance, None]:
|
||||
file = tool.Ifc.get()
|
||||
projects = file.by_type("IfcProject")
|
||||
return projects[0] if projects else None
|
||||
|
||||
@classmethod
|
||||
def get_selected_document_information(cls) -> Union[ifcopenshell.entity_instance, None]:
|
||||
props = cls.get_document_props()
|
||||
|
||||
if props.active_document and props.active_document.document_type == "INFORMATION":
|
||||
file = tool.Ifc.get()
|
||||
return file.by_id(props.active_document.ifc_definition_id)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def refresh_document_data(cls) -> None:
|
||||
import bonsai.bim.module.document.data as document_data
|
||||
|
||||
document_data.DocumentData.is_loaded = False
|
||||
document_data.DocumentData.load()
|
||||
|
||||
@classmethod
|
||||
def load_document_objects_into_props(cls, document_id: int) -> None:
|
||||
import bonsai.bim.module.document.data as document_data
|
||||
|
||||
document_data.DocumentData.load_document_objects_into_props(document_id)
|
||||
|
||||
@classmethod
|
||||
def update_document_objects(cls, document_id: Union[int, None] = None) -> None:
|
||||
cls.refresh_document_data()
|
||||
|
||||
if document_id is None:
|
||||
props = cls.get_document_props()
|
||||
if props.active_document and props.active_document.ifc_definition_id > 0:
|
||||
document_id = props.active_document.ifc_definition_id
|
||||
|
||||
if document_id:
|
||||
cls.load_document_objects_into_props(document_id)
|
||||
|
||||
@@ -1021,7 +1021,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
elif material.is_a("IfcMaterialLayerSetUsage"):
|
||||
usage = material
|
||||
layer_set = material.ForLayerSet
|
||||
offset = usage.OffsetFromReferenceLine * cls.unit_scale
|
||||
offset = usage.OffsetFromReferenceLine
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
usage = None
|
||||
@@ -1030,61 +1030,168 @@ class Loader(bonsai.core.tool.Loader):
|
||||
sense_factor = 1
|
||||
else:
|
||||
return mesh
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
return mesh
|
||||
|
||||
# Get mesh bounds
|
||||
if len(mesh.vertices) > 0:
|
||||
z_coords = [v.co.z for v in mesh.vertices]
|
||||
mesh_z_min = min(z_coords)
|
||||
mesh_z_max = max(z_coords)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
prev_co = None
|
||||
advance_direction = None
|
||||
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
sense_factor = 1
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
advance_direction = no
|
||||
elif usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
# Get local extrusion direction
|
||||
local_extrusion = Vector([0.0, 0.0, 1.0])
|
||||
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
local_extrusion = Vector(item.ExtrudedDirection.DirectionRatios).normalized()
|
||||
break
|
||||
|
||||
# Thickness direction: perpendicular to extrusion and length
|
||||
thickness_dir = local_extrusion.cross(Vector([1.0, 0.0, 0.0])).normalized()
|
||||
if thickness_dir.y < 0:
|
||||
thickness_dir = -thickness_dir
|
||||
|
||||
no = thickness_dir
|
||||
|
||||
# Find start point by projecting vertices onto thickness direction
|
||||
if len(mesh.vertices) > 0:
|
||||
projections = [Vector(v.co).dot(no) for v in mesh.vertices]
|
||||
min_proj = min(projections)
|
||||
max_proj = max(projections)
|
||||
|
||||
centroid = sum((Vector(v.co) for v in mesh.vertices), Vector()) / len(mesh.vertices)
|
||||
centroid_proj = centroid.dot(no)
|
||||
|
||||
if sense_factor == 1:
|
||||
start_proj = min_proj
|
||||
else:
|
||||
start_proj = max_proj
|
||||
|
||||
offset_dist = start_proj - centroid_proj
|
||||
co = centroid + no * offset_dist
|
||||
|
||||
actual_mesh_height = max_proj - min_proj
|
||||
else:
|
||||
co = Vector((0.0, 0.0, 0.0))
|
||||
|
||||
advance_direction = thickness_dir
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
# AXIS3 layers go through slab thickness (local Z)
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
|
||||
# Find start point by projecting vertices onto Z direction
|
||||
if len(mesh.vertices) > 0:
|
||||
projections = [Vector(v.co).dot(no) for v in mesh.vertices]
|
||||
min_proj = min(projections)
|
||||
max_proj = max(projections)
|
||||
|
||||
centroid = sum((Vector(v.co) for v in mesh.vertices), Vector()) / len(mesh.vertices)
|
||||
centroid_proj = centroid.dot(no)
|
||||
|
||||
if sense_factor == 1:
|
||||
start_proj = min_proj
|
||||
else:
|
||||
start_proj = max_proj
|
||||
|
||||
offset = start_proj - centroid_proj
|
||||
co = centroid + no * offset
|
||||
|
||||
actual_mesh_height = max_proj - min_proj
|
||||
else:
|
||||
co = Vector((0.0, 0.0, 0.0))
|
||||
|
||||
advance_direction = no
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
no *= sense_factor
|
||||
# Cache this
|
||||
advance_direction = no
|
||||
|
||||
# Apply DirectionSense
|
||||
if usage and usage.LayerSetDirection == "AXIS2":
|
||||
if sense_factor == -1:
|
||||
advance_direction = -advance_direction
|
||||
test_normal = -no
|
||||
else:
|
||||
test_normal = no
|
||||
elif usage and usage.LayerSetDirection == "AXIS1":
|
||||
no = no * sense_factor
|
||||
advance_direction = advance_direction * sense_factor
|
||||
test_normal = no
|
||||
elif usage and usage.LayerSetDirection == "AXIS3":
|
||||
if sense_factor == -1:
|
||||
advance_direction = -advance_direction
|
||||
test_normal = -no
|
||||
else:
|
||||
test_normal = no
|
||||
else:
|
||||
test_normal = no
|
||||
|
||||
# Cache material styles
|
||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
styles = {}
|
||||
has_layer_styles = False
|
||||
for i, material in enumerate(mesh.materials):
|
||||
if style := tool.Ifc.get_entity(material):
|
||||
styles[style] = i
|
||||
|
||||
layer_list = list(enumerate(layer_set.MaterialLayers))
|
||||
|
||||
# Calculate scale factor
|
||||
total_layer_thickness = sum(layer.LayerThickness for _, layer in layer_list)
|
||||
|
||||
if 'actual_mesh_height' not in locals():
|
||||
actual_mesh_height = mesh_z_max - mesh_z_min if len(mesh.vertices) > 0 else total_layer_thickness
|
||||
|
||||
thickness_scale = actual_mesh_height / total_layer_thickness if total_layer_thickness > 0 else 1.0
|
||||
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
if i != last_i:
|
||||
|
||||
for idx, (original_i, layer) in enumerate(layer_list):
|
||||
if idx != last_i:
|
||||
prev_co = co.copy()
|
||||
co += no * layer.LayerThickness * cls.unit_scale
|
||||
advance_vector = advance_direction * layer.LayerThickness * thickness_scale
|
||||
co += advance_vector
|
||||
|
||||
bisect_geom = bmesh.ops.bisect_plane(
|
||||
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
|
||||
)
|
||||
bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"])
|
||||
|
||||
if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)):
|
||||
continue
|
||||
if (material_index := styles.get(style, None)) is None:
|
||||
material_index = len(mesh.materials)
|
||||
mesh.materials.append(tool.Ifc.get_object(style))
|
||||
if i == last_i:
|
||||
|
||||
if idx == last_i:
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
center = face.calc_center_median()
|
||||
if (center - co).dot(no) >= 0:
|
||||
if (center - co).dot(test_normal) >= 0:
|
||||
face.material_index = material_index
|
||||
has_layer_styles = True
|
||||
else:
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
center = face.calc_center_median()
|
||||
if (center - co).dot(no) < 0 and (center - prev_co).dot(no) >= 0:
|
||||
if (center - co).dot(test_normal) < 0 and (center - prev_co).dot(test_normal) >= 0:
|
||||
face.material_index = material_index
|
||||
has_layer_styles = True
|
||||
|
||||
@@ -1097,13 +1204,35 @@ class Loader(bonsai.core.tool.Loader):
|
||||
return mesh
|
||||
|
||||
@classmethod
|
||||
def get_extrusion_vector(cls, wall):
|
||||
if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"):
|
||||
def get_extrusion_vector(cls, element):
|
||||
"""Get the extrusion direction in WORLD coordinates (accounting for object rotation)"""
|
||||
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
return Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
|
||||
# Transform to world coordinates using object rotation
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
# Apply object rotation to get actual world direction
|
||||
world_direction = obj.matrix_world.to_3x3() @ local_direction
|
||||
return world_direction
|
||||
|
||||
return local_direction
|
||||
return Vector([0.0, 0.0, 1.0])
|
||||
|
||||
@classmethod
|
||||
def get_local_extrusion_vector(cls, element):
|
||||
"""Get the extrusion direction in LOCAL coordinates (from IFC, no object rotation)"""
|
||||
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
return local_direction
|
||||
return Vector([0.0, 0.0, 1.0])
|
||||
|
||||
@classmethod
|
||||
|
||||
+103
-18
@@ -620,6 +620,71 @@ class Model(bonsai.core.tool.Model):
|
||||
if not openings[i].obj:
|
||||
openings.remove(i)
|
||||
|
||||
@classmethod
|
||||
def save_custom_offset_to_pset(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
|
||||
"""Save custom offset settings to BBIM_MaterialLayer pset."""
|
||||
props = tool.Material.get_object_material_props(obj)
|
||||
|
||||
if not props.use_custom_offset:
|
||||
# Remove pset if custom offset is disabled
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
||||
if pset:
|
||||
pset_entity = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset_entity)
|
||||
return
|
||||
|
||||
# Determine which reference to save based on usage type
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
custom_wall_reference = None
|
||||
custom_slab_reference = None
|
||||
|
||||
if usage_type == "LAYER2":
|
||||
custom_wall_reference = props.custom_wall_reference
|
||||
elif usage_type == "LAYER3":
|
||||
custom_slab_reference = props.custom_slab_reference
|
||||
|
||||
# Get or create pset
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
||||
if pset_data:
|
||||
pset = tool.Ifc.get().by_id(pset_data["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="BBIM_MaterialLayer")
|
||||
|
||||
# Save properties (store in SI units)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
properties = {
|
||||
"UseCustomOffset": props.use_custom_offset,
|
||||
"CustomOffset": props.custom_offset / unit_scale,
|
||||
"CustomWallReference": custom_wall_reference if custom_wall_reference else "",
|
||||
"CustomSlabReference": custom_slab_reference if custom_slab_reference else "",
|
||||
}
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=properties)
|
||||
|
||||
@classmethod
|
||||
def load_custom_offset_from_pset(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
|
||||
"""Load custom offset settings from BBIM_MaterialLayer pset."""
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
||||
if not pset:
|
||||
return
|
||||
|
||||
props = tool.Material.get_object_material_props(obj)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
# Load properties
|
||||
props.use_custom_offset = pset.get("UseCustomOffset", False)
|
||||
props.custom_offset = pset.get("CustomOffset", 0.0) * unit_scale # Convert from SI
|
||||
|
||||
# Load the appropriate reference based on usage type
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
if usage_type == "LAYER2":
|
||||
custom_wall_ref = pset.get("CustomWallReference", "")
|
||||
if custom_wall_ref:
|
||||
props.custom_wall_reference = custom_wall_ref
|
||||
elif usage_type == "LAYER3":
|
||||
custom_slab_ref = pset.get("CustomSlabReference", "")
|
||||
if custom_slab_ref:
|
||||
props.custom_slab_reference = custom_slab_ref
|
||||
|
||||
class MaterialLayerParameters(TypedDict):
|
||||
"""Float values are in project units."""
|
||||
|
||||
@@ -652,13 +717,35 @@ class Model(bonsai.core.tool.Model):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_material_layer_custom_offset(cls, element: ifcopenshell.entity_instance, obj) -> MaterialLayerParameters:
|
||||
def get_material_layer_custom_offset(
|
||||
cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object
|
||||
) -> Optional[float]:
|
||||
"""Get custom offset value, reading from pset if props are not set."""
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||
layer_offset = layer_params["offset"]
|
||||
thickness = layer_params["thickness"] / unit_scale
|
||||
props = tool.Material.get_object_material_props(obj)
|
||||
if props.use_custom_offset:
|
||||
|
||||
# Try to load from pset if not already in props
|
||||
if not props.use_custom_offset:
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
||||
if pset and pset.get("UseCustomOffset", False):
|
||||
# Load from pset
|
||||
custom_offset = pset.get("CustomOffset", 0.0)
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
|
||||
if usage_type == "LAYER2":
|
||||
custom_offset_reference = pset.get("CustomWallReference", "CENTER")
|
||||
elif usage_type == "LAYER3":
|
||||
custom_offset_reference = pset.get("CustomSlabReference", "MIDDLE")
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
# Use current props
|
||||
custom_offset = props.custom_offset / unit_scale
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
custom_offset_reference = props.custom_wall_reference
|
||||
elif tool.Model.get_usage_type(element) == "LAYER3":
|
||||
@@ -666,24 +753,22 @@ class Model(bonsai.core.tool.Model):
|
||||
else:
|
||||
return None
|
||||
|
||||
custom_offset = props.custom_offset
|
||||
direction_sense = layer_params["direction_sense"]
|
||||
if direction_sense == "POSITIVE" and custom_offset_reference in {"INTERIOR", "TOP"}:
|
||||
layer_offset = custom_offset - thickness * unit_scale
|
||||
if direction_sense == "POSITIVE" and custom_offset_reference in {"CENTER", "MIDDLE"}:
|
||||
layer_offset = custom_offset - (thickness / 2) * unit_scale
|
||||
if (direction_sense == "POSITIVE" and custom_offset_reference in {"EXTERIOR", "BOTTOM"}) or (
|
||||
direction_sense == "NEGATIVE" and custom_offset_reference in {"EXTERIOR", "TOP"}
|
||||
):
|
||||
layer_offset = custom_offset
|
||||
if direction_sense == "NEGATIVE" and custom_offset_reference in {"CENTER", "MIDDLE"}:
|
||||
layer_offset = custom_offset + (thickness / 2) * unit_scale
|
||||
if direction_sense == "NEGATIVE" and custom_offset_reference in {"INTERIOR", "BOTTOM"}:
|
||||
layer_offset = custom_offset + thickness * unit_scale
|
||||
direction_sense = layer_params["direction_sense"]
|
||||
|
||||
return layer_offset / unit_scale
|
||||
if direction_sense == "POSITIVE" and custom_offset_reference in {"INTERIOR", "TOP"}:
|
||||
layer_offset = custom_offset - thickness * unit_scale
|
||||
if direction_sense == "POSITIVE" and custom_offset_reference in {"CENTER", "MIDDLE"}:
|
||||
layer_offset = custom_offset - (thickness / 2) * unit_scale
|
||||
if (direction_sense == "POSITIVE" and custom_offset_reference in {"EXTERIOR", "BOTTOM"}) or (
|
||||
direction_sense == "NEGATIVE" and custom_offset_reference in {"EXTERIOR", "TOP"}
|
||||
):
|
||||
layer_offset = custom_offset
|
||||
if direction_sense == "NEGATIVE" and custom_offset_reference in {"CENTER", "MIDDLE"}:
|
||||
layer_offset = custom_offset + (thickness / 2) * unit_scale
|
||||
if direction_sense == "NEGATIVE" and custom_offset_reference in {"INTERIOR", "BOTTOM"}:
|
||||
layer_offset = custom_offset + thickness * unit_scale
|
||||
|
||||
return None
|
||||
return layer_offset / unit_scale
|
||||
|
||||
@classmethod
|
||||
def get_booleans(
|
||||
|
||||
@@ -513,3 +513,66 @@ class Project(bonsai.core.tool.Project):
|
||||
if tmp.exists():
|
||||
shutil.rmtree(tmp)
|
||||
bpy.ops.bim.save_project(filepath=cls.TEMP_PROJECT_PATH.__str__(), should_save_as=True)
|
||||
|
||||
@classmethod
|
||||
def get_metadata_document_information(cls) -> Optional[ifcopenshell.entity_instance]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
if not ifc_file:
|
||||
return None
|
||||
for doc in ifc_file.by_type("IfcDocumentInformation"):
|
||||
if getattr(doc, "Scope", None) == "BLEND_METADATA":
|
||||
return doc
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def create_metadata_document_information(cls, metadata_filename: str) -> ifcopenshell.entity_instance:
|
||||
ifc_file = tool.Ifc.get()
|
||||
if not ifc_file:
|
||||
raise Exception("No IFC file loaded")
|
||||
|
||||
doc = tool.Ifc.run("document.add_information", parent=None)
|
||||
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
tool.Ifc.run(
|
||||
"document.edit_information",
|
||||
information=doc,
|
||||
attributes={
|
||||
"DocumentId": "BLEND_METADATA",
|
||||
"Name": "Blend Metadata",
|
||||
"Scope": "BLEND_METADATA",
|
||||
"Description": "References to blend metadata file for this IFC project",
|
||||
"Location": metadata_filename,
|
||||
},
|
||||
)
|
||||
else:
|
||||
tool.Ifc.run(
|
||||
"document.edit_information",
|
||||
information=doc,
|
||||
attributes={
|
||||
"Identification": "BLEND_METADATA",
|
||||
"Name": "Blend Metadata",
|
||||
"Scope": "BLEND_METADATA",
|
||||
"Description": "References to blend metadata file for this IFC project",
|
||||
"Location": metadata_filename,
|
||||
},
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
@classmethod
|
||||
def update_metadata_document_information(cls, metadata_filename: str) -> None:
|
||||
doc = cls.get_metadata_document_information()
|
||||
if not doc:
|
||||
return
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
if not ifc_file:
|
||||
return
|
||||
|
||||
tool.Ifc.run("document.edit_information", information=doc, attributes={"Location": metadata_filename})
|
||||
|
||||
@classmethod
|
||||
def remove_metadata_document_information(cls) -> None:
|
||||
doc = cls.get_metadata_document_information()
|
||||
if doc:
|
||||
tool.Ifc.run("document.remove_information", information=doc)
|
||||
|
||||
@@ -553,7 +553,11 @@ class Snap(bonsai.core.tool.Snap):
|
||||
def filter_snapping_points_by_type(snapping_points):
|
||||
options = ["Plane", "Axis"]
|
||||
props = tool.Snap.get_snap_props()
|
||||
for prop in props.__annotations__.keys():
|
||||
try:
|
||||
annotations = props.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(props).__annotations__
|
||||
for prop in annotations.keys():
|
||||
if getattr(props, prop):
|
||||
options.append(props.rna_type.properties[prop].name)
|
||||
|
||||
@@ -563,7 +567,11 @@ class Snap(bonsai.core.tool.Snap):
|
||||
def filter_snapping_points_by_group(detected_snaps):
|
||||
options = ["Wireframe", "Axis", "Plane"]
|
||||
props = tool.Snap.get_snap_groups()
|
||||
for prop in props.__annotations__.keys():
|
||||
try:
|
||||
annotations = props.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(props).__annotations__
|
||||
for prop in annotations.keys():
|
||||
if getattr(props, prop):
|
||||
options.append(props.rna_type.properties[prop].name)
|
||||
filtered_groups = [group for group in detected_snaps if group["group"] in options]
|
||||
|
||||
@@ -74,7 +74,7 @@ class Type(bonsai.core.tool.Type):
|
||||
def get_model_types(cls) -> list[ifcopenshell.entity_instance]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
types = ifc_file.by_type("IfcElementType")
|
||||
# exclude IfcSpatialElementType
|
||||
types += ifc_file.by_type("IfcSpatialElementType")
|
||||
types += ifc_file.by_type("IfcTypeProduct", include_subtypes=False)
|
||||
if not tool.Ifc.get_schema().startswith("IFC4X3"):
|
||||
types += ifc_file.by_type("IfcWindowStyle")
|
||||
|
||||
@@ -166,6 +166,11 @@ def parse_distance_string(input_string: str, use_project_unit: bool = True) -> t
|
||||
break
|
||||
if inches is None:
|
||||
inches = 0
|
||||
|
||||
# If feet is negative, inches should also be negative (subtractive)
|
||||
if feet < 0:
|
||||
inches = -inches
|
||||
|
||||
# Convert to meters
|
||||
total_meters = (feet * 0.3048) + (inches * 0.0254)
|
||||
return total_meters
|
||||
|
||||
@@ -200,6 +200,7 @@ Packaged installation
|
||||
|
||||
- **Arch Linux**: `Direct from Git <https://aur.archlinux.org/packages/ifcopenshell-git/>`__.
|
||||
- **Chocolatey on Windows**: `Unstable <https://community.chocolatey.org/packages/bonsai-nightly/>`__.
|
||||
- **Fedora Linux**: `IfcOpenShell Copr repository <https://copr.fedorainfracloud.org/coprs/bpostle/IfcOpenShell/>`__.
|
||||
|
||||
Tips for package managers
|
||||
-------------------------
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import bonsai, json, bpy
|
||||
from pathlib import Path
|
||||
|
||||
repo_path = Path(bonsai.__file__).resolve().parent
|
||||
install_path = Path(bonsai.__file__).absolute().parent
|
||||
assert repo_path != install_path, "Run `dev_environment.py` to setup the development environment symlinks first."
|
||||
|
||||
repo_root = repo_path.parent.parent.parent
|
||||
settings_path = repo_root / ".vscode" / "settings.json"
|
||||
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||
settings.update(
|
||||
{
|
||||
"bonsai.localRoot": repo_path.as_posix(),
|
||||
"bonsai.remoteRoot": install_path.as_posix(),
|
||||
"bonsai.blenderPath": Path(bpy.app.binary_path).parent.as_posix(),
|
||||
}
|
||||
)
|
||||
json_data = json.dumps(settings, indent=2)
|
||||
|
||||
settings_path.write_text(json_data)
|
||||
|
||||
print("\n\nBonsai/VSCode development environment configured successfully!\n\n")
|
||||
@@ -6,23 +6,6 @@ Scenario: Load project documents
|
||||
When I press "bim.load_project_documents"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Load document
|
||||
Given an empty IFC project
|
||||
And I press "bim.load_project_documents"
|
||||
And I press "bim.add_information"
|
||||
And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()"
|
||||
When I press "bim.load_document(document={information})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Load parent document
|
||||
Given an empty IFC project
|
||||
And I press "bim.load_project_documents"
|
||||
And I press "bim.add_information"
|
||||
And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()"
|
||||
And I press "bim.load_document(document={information})"
|
||||
When I press "bim.load_parent_document"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable document editing UI
|
||||
Given an empty IFC project
|
||||
And I press "bim.load_project_documents"
|
||||
@@ -57,7 +40,8 @@ Scenario: Add document reference
|
||||
And I press "bim.load_project_documents"
|
||||
And I press "bim.add_information"
|
||||
And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()"
|
||||
And I press "bim.load_document(document={information})"
|
||||
And I press "bim.load_project_documents"
|
||||
And I set "scene.BIMDocumentProperties.active_document_index" to "1"
|
||||
When I press "bim.add_document_reference"
|
||||
Then nothing happens
|
||||
|
||||
@@ -83,16 +67,12 @@ Scenario: Assign document
|
||||
And I press "bim.load_project_documents"
|
||||
And I press "bim.add_information"
|
||||
And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()"
|
||||
And I press "bim.load_document(document={information})"
|
||||
And I press "bim.add_document_reference"
|
||||
And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
When I press "bim.assign_document(document={reference})"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
When I press "bim.assign_document(document={information})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Unassign document
|
||||
@@ -100,15 +80,11 @@ Scenario: Unassign document
|
||||
And I press "bim.load_project_documents"
|
||||
And I press "bim.add_information"
|
||||
And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()"
|
||||
And I press "bim.load_document(document={information})"
|
||||
And I press "bim.add_document_reference"
|
||||
And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.assign_document(document={reference})"
|
||||
When I press "bim.unassign_document(document={reference})"
|
||||
Then nothing happens
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.assign_document(document={information})"
|
||||
When I press "bim.unassign_document(document={information})"
|
||||
Then nothing happens
|
||||
@@ -96,7 +96,11 @@ class PanelSpy:
|
||||
|
||||
def __getattr__(self, attr: str) -> PanelSpy | Any:
|
||||
self.spied_attr = attr
|
||||
if annotation := self.blender_panel.__annotations__.get(attr, None):
|
||||
try:
|
||||
annotations = self.blender_panel.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(self.blender_panel).__annotations__
|
||||
if annotation := annotations.get(attr, None):
|
||||
return annotation.keywords.get("default", None) # An operator property
|
||||
if attr == "layout":
|
||||
return self
|
||||
@@ -136,7 +140,11 @@ class PanelSpy:
|
||||
prop_type = props.bl_rna.properties[name].type
|
||||
enum_items = []
|
||||
if prop_type == "ENUM":
|
||||
prop_keywords = props.__annotations__[name].keywords
|
||||
try:
|
||||
annotations = props.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(props).__annotations__
|
||||
prop_keywords = annotations[name].keywords
|
||||
items = prop_keywords.get("items")
|
||||
if items is not None:
|
||||
if isinstance(items, (list, tuple)):
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import bonsai.core.document as subject
|
||||
from test.core.bootstrap import ifc, document
|
||||
|
||||
@@ -25,21 +24,10 @@ class TestLoadProjectDocuments:
|
||||
def test_run(self, document):
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.import_project_documents().should_be_called()
|
||||
document.clear_breadcrumbs().should_be_called()
|
||||
document.enable_editing_ui().should_be_called()
|
||||
subject.load_project_documents(document)
|
||||
|
||||
|
||||
class TestLoadDocument:
|
||||
def test_run(self, document):
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.import_subdocuments("document").should_be_called()
|
||||
document.import_references("document").should_be_called()
|
||||
document.disable_editing_document().should_be_called()
|
||||
document.add_breadcrumb("document").should_be_called()
|
||||
subject.load_document(document, document="document")
|
||||
|
||||
|
||||
class TestDisableDocumentEditingUi:
|
||||
def test_run(self, document):
|
||||
document.disable_editing_ui().should_be_called()
|
||||
@@ -47,45 +35,71 @@ class TestDisableDocumentEditingUi:
|
||||
subject.disable_document_editing_ui(document)
|
||||
|
||||
|
||||
class TestDisableObjectDocumentEditingUi:
|
||||
def test_run(self, document):
|
||||
document.disable_object_editing_ui().should_be_called()
|
||||
subject.disable_object_document_editing_ui(document)
|
||||
|
||||
|
||||
class TestEnableEditingDocument:
|
||||
def test_run(self, document):
|
||||
document.import_document_attributes("document").should_be_called()
|
||||
document.set_active_document("document").should_be_called()
|
||||
subject.enable_editing_document(document, document="document")
|
||||
document.import_document_attributes("document").should_be_called()
|
||||
subject.enable_editing_document(document, ifc_document="document")
|
||||
|
||||
|
||||
class TestDisableEditingDocument:
|
||||
def test_run(self, document):
|
||||
document.disable_editing_document().should_be_called()
|
||||
document.clear_active_document().should_be_called()
|
||||
document.clear_document_attributes().should_be_called()
|
||||
subject.disable_editing_document(document)
|
||||
|
||||
|
||||
class TestAddInformation:
|
||||
def test_add_and_reload_tree_at_project_root(self, ifc, document):
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.get_active_breadcrumb().should_be_called().will_return(None)
|
||||
ifc.run("document.add_information", parent=None).should_be_called().will_return("information")
|
||||
document.get_default_parent_for_information().should_be_called().will_return("default_parent")
|
||||
ifc.run("document.add_information", parent="default_parent").should_be_called().will_return("information")
|
||||
ifc.run("document.add_reference", information="information").should_be_called()
|
||||
document.is_document_information("default_parent").should_be_called().will_return(True)
|
||||
document.expand_document("default_parent").should_be_called()
|
||||
document.import_project_documents().should_be_called()
|
||||
|
||||
subject.add_information(ifc, document)
|
||||
|
||||
def test_add_and_reload_tree_at_current_parent(self, ifc, document):
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.get_active_breadcrumb().should_be_called().will_return("parent")
|
||||
ifc.run("document.add_information", parent="parent").should_be_called().will_return("information")
|
||||
ifc.run("document.add_reference", information="information").should_be_called()
|
||||
document.import_subdocuments("parent").should_be_called()
|
||||
document.import_references("parent").should_be_called()
|
||||
subject.add_information(ifc, document)
|
||||
document.is_document_information("parent").should_be_called().will_return(True)
|
||||
document.expand_document("parent").should_be_called()
|
||||
document.import_project_documents().should_be_called()
|
||||
|
||||
subject.add_information(ifc, document, parent="parent")
|
||||
|
||||
def test_add_without_expanding_if_parent_is_not_information(self, ifc, document):
|
||||
document.clear_document_tree().should_be_called()
|
||||
ifc.run("document.add_information", parent="parent").should_be_called().will_return("information")
|
||||
ifc.run("document.add_reference", information="information").should_be_called()
|
||||
document.is_document_information("parent").should_be_called().will_return(False)
|
||||
document.import_project_documents().should_be_called()
|
||||
|
||||
subject.add_information(ifc, document, parent="parent")
|
||||
|
||||
|
||||
class TestAddReference:
|
||||
def test_run(self, ifc, document):
|
||||
document.get_active_breadcrumb().should_be_called().will_return("parent")
|
||||
def test_run_with_selected_parent(self, ifc, document):
|
||||
document.get_selected_document_information().should_be_called().will_return("parent")
|
||||
ifc.run("document.add_reference", information="parent").should_be_called()
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.import_subdocuments("parent").should_be_called()
|
||||
document.import_references("parent").should_be_called()
|
||||
document.expand_document("parent").should_be_called()
|
||||
document.import_project_documents().should_be_called()
|
||||
|
||||
subject.add_reference(ifc, document)
|
||||
|
||||
def test_run_without_selected_parent(self, ifc, document):
|
||||
document.get_selected_document_information().should_be_called().will_return(None)
|
||||
document.import_project_documents().should_be_called()
|
||||
|
||||
subject.add_reference(ifc, document)
|
||||
|
||||
|
||||
@@ -96,9 +110,8 @@ class TestEditDocument:
|
||||
ifc.run("document.edit_information", information="document", attributes="attributes").should_be_called()
|
||||
document.disable_editing_document().should_be_called()
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.get_active_breadcrumb().should_be_called().will_return(None)
|
||||
document.import_project_documents().should_be_called()
|
||||
subject.edit_document(ifc, document, document="document")
|
||||
subject.edit_document(ifc, document, ifc_document="document")
|
||||
|
||||
def test_edit_reference(self, ifc, document):
|
||||
document.export_document_attributes().should_be_called().will_return("attributes")
|
||||
@@ -106,10 +119,8 @@ class TestEditDocument:
|
||||
ifc.run("document.edit_reference", reference="document", attributes="attributes").should_be_called()
|
||||
document.disable_editing_document().should_be_called()
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.get_active_breadcrumb().should_be_called().will_return("parent")
|
||||
document.import_subdocuments("parent").should_be_called()
|
||||
document.import_references("parent").should_be_called()
|
||||
subject.edit_document(ifc, document, document="document")
|
||||
document.import_project_documents().should_be_called()
|
||||
subject.edit_document(ifc, document, ifc_document="document")
|
||||
|
||||
|
||||
class TestRemoveDocument:
|
||||
@@ -117,27 +128,24 @@ class TestRemoveDocument:
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.is_document_information("document").should_be_called().will_return(True)
|
||||
ifc.run("document.remove_information", information="document").should_be_called()
|
||||
document.get_active_breadcrumb().should_be_called().will_return(None)
|
||||
document.import_project_documents().should_be_called()
|
||||
subject.remove_document(ifc, document, document="document")
|
||||
subject.remove_document(ifc, document, ifc_document="document")
|
||||
|
||||
def test_remove_reference(self, ifc, document):
|
||||
document.clear_document_tree().should_be_called()
|
||||
document.is_document_information("document").should_be_called().will_return(False)
|
||||
ifc.run("document.remove_reference", reference="document").should_be_called()
|
||||
document.get_active_breadcrumb().should_be_called().will_return("parent")
|
||||
document.import_subdocuments("parent").should_be_called()
|
||||
document.import_references("parent").should_be_called()
|
||||
subject.remove_document(ifc, document, document="document")
|
||||
document.import_project_documents().should_be_called()
|
||||
subject.remove_document(ifc, document, ifc_document="document")
|
||||
|
||||
|
||||
class TestAssignDocument:
|
||||
def test_run(self, ifc):
|
||||
ifc.run("document.assign_document", products=["product"], document="document").should_be_called()
|
||||
subject.assign_document(ifc, product="product", document="document")
|
||||
subject.assign_document(ifc, product="product", ifc_document="document")
|
||||
|
||||
|
||||
class TestUnassignDocument:
|
||||
def test_run(self, ifc):
|
||||
ifc.run("document.unassign_document", products=["product"], document="document").should_be_called()
|
||||
subject.unassign_document(ifc, product="product", document="document")
|
||||
subject.unassign_document(ifc, product="product", ifc_document="document")
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell.api
|
||||
import ifcopenshell.api.document
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import json
|
||||
from test.bim.bootstrap import NewFile
|
||||
from bonsai.tool.document import Document as subject
|
||||
|
||||
@@ -31,24 +32,6 @@ class TestImplementsTool(NewFile):
|
||||
assert isinstance(subject(), bonsai.core.tool.Document)
|
||||
|
||||
|
||||
class TestAddBreadcrumb(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc().set(ifc)
|
||||
document = ifc.createIfcDocumentInformation()
|
||||
subject.add_breadcrumb(document)
|
||||
props = tool.Document.get_document_props()
|
||||
assert props.breadcrumbs[0].name == str(document.id())
|
||||
|
||||
|
||||
class TestClearBreadcrumbs(NewFile):
|
||||
def test_run(self):
|
||||
props = tool.Document.get_document_props()
|
||||
props.breadcrumbs.add()
|
||||
subject.clear_breadcrumbs()
|
||||
assert len(props.breadcrumbs) == 0
|
||||
|
||||
|
||||
class TestClearDocumentTree(NewFile):
|
||||
def test_run(self):
|
||||
props = tool.Document.get_document_props()
|
||||
@@ -103,15 +86,6 @@ class TestExportDocumentAttributes(NewFile):
|
||||
}
|
||||
|
||||
|
||||
class TestGetActiveBreadcrumb(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc().set(ifc)
|
||||
document = ifc.createIfcDocumentInformation()
|
||||
subject.add_breadcrumb(document)
|
||||
assert subject.get_active_breadcrumb() == document
|
||||
|
||||
|
||||
class TestImportDocumentAttributes(NewFile):
|
||||
def test_importing_information(self):
|
||||
ifc = ifcopenshell.file()
|
||||
@@ -166,51 +140,64 @@ class TestImportDocumentAttributes(NewFile):
|
||||
assert props.document_attributes["Description"].string_value == "Description"
|
||||
|
||||
|
||||
class TestImportProjectDocuments(NewFile):
|
||||
class TestImportProjectDocumentsExpanded(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc().set(ifc)
|
||||
ifc.createIfcProject()
|
||||
document = ifcopenshell.api.document.add_information(ifc)
|
||||
subject.import_project_documents()
|
||||
props = tool.Document.get_document_props()
|
||||
assert len(props.documents) == 1
|
||||
assert props.documents[0].ifc_definition_id == document.id()
|
||||
assert props.documents[0].name == "Unnamed"
|
||||
assert props.documents[0].identification == "X"
|
||||
assert props.documents[0].is_information is True
|
||||
|
||||
|
||||
class TestImportReferences(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc().set(ifc)
|
||||
ifc.createIfcProject()
|
||||
project = ifc.createIfcProject()
|
||||
document = ifcopenshell.api.document.add_information(ifc)
|
||||
reference = ifcopenshell.api.document.add_reference(ifc, information=document)
|
||||
subject.import_references(document)
|
||||
|
||||
props = tool.Document.get_document_props()
|
||||
assert len(props.documents) == 1
|
||||
assert props.documents[0].ifc_definition_id == reference.id()
|
||||
assert props.documents[0].name == "Unnamed"
|
||||
assert props.documents[0].identification == "X"
|
||||
assert props.documents[0].is_information is False
|
||||
expanded_docs = [document.id()] # Mark document as expanded
|
||||
props.json_string = json.dumps(expanded_docs)
|
||||
|
||||
subject.import_project_documents()
|
||||
props = tool.Document.get_document_props()
|
||||
|
||||
# Should have project root + document + reference = 3 total
|
||||
assert len(props.documents) == 3
|
||||
|
||||
assert props.documents[0].ifc_definition_id == -project.id()
|
||||
assert props.documents[0].document_type == "PROJECT"
|
||||
|
||||
doc_info = next((d for d in props.documents if d.ifc_definition_id == document.id()), None)
|
||||
assert doc_info is not None
|
||||
assert doc_info.document_type == "INFORMATION"
|
||||
|
||||
doc_ref = next((d for d in props.documents if d.ifc_definition_id == reference.id()), None)
|
||||
assert doc_ref is not None
|
||||
assert doc_ref.location == ""
|
||||
assert doc_ref.identification == "X"
|
||||
assert doc_ref.document_type == "REFERENCE"
|
||||
|
||||
|
||||
class TestImportSubdocuments(NewFile):
|
||||
class TestImportProjectDocumentsCollapsed(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc().set(ifc)
|
||||
ifc.createIfcProject()
|
||||
project = ifc.createIfcProject()
|
||||
document = ifcopenshell.api.document.add_information(ifc)
|
||||
subdocument = ifcopenshell.api.document.add_information(ifc, parent=document)
|
||||
subject.import_subdocuments(document)
|
||||
reference = ifcopenshell.api.document.add_reference(ifc, information=document)
|
||||
|
||||
props = tool.Document.get_document_props()
|
||||
assert len(props.documents) == 1
|
||||
assert props.documents[0].ifc_definition_id == subdocument.id()
|
||||
assert props.documents[0].name == "Unnamed"
|
||||
assert props.documents[0].identification == "X"
|
||||
assert props.documents[0].is_information is True
|
||||
props.json_string = json.dumps([]) # Empty expanded list
|
||||
|
||||
subject.import_project_documents()
|
||||
props = tool.Document.get_document_props()
|
||||
|
||||
# Should have project root + document = 2 total (reference not imported because parent is collapsed)
|
||||
assert len(props.documents) == 2
|
||||
|
||||
assert props.documents[0].ifc_definition_id == -project.id()
|
||||
assert props.documents[0].document_type == "PROJECT"
|
||||
|
||||
doc_info = next((d for d in props.documents if d.ifc_definition_id == document.id()), None)
|
||||
assert doc_info is not None
|
||||
assert doc_info.document_type == "INFORMATION"
|
||||
|
||||
doc_ref = next((d for d in props.documents if d.ifc_definition_id == reference.id()), None)
|
||||
assert doc_ref is None
|
||||
|
||||
|
||||
class TestIsDocumentInformation(NewFile):
|
||||
@@ -222,15 +209,6 @@ class TestIsDocumentInformation(NewFile):
|
||||
assert subject.is_document_information(reference) is False
|
||||
|
||||
|
||||
class TestRemoveLatestBreadcrumb(NewFile):
|
||||
def test_run(self):
|
||||
props = tool.Document.get_document_props()
|
||||
props.breadcrumbs.add()
|
||||
props.breadcrumbs.add()
|
||||
subject.remove_latest_breadcrumb()
|
||||
assert len(props.breadcrumbs) == 1
|
||||
|
||||
|
||||
class TestSetActiveDocument(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
|
||||
+4
-6
@@ -184,7 +184,7 @@ class ClassPropertyContractV1(TypedDict):
|
||||
qudtCodes: NotRequired[list[str]]
|
||||
|
||||
|
||||
class PropertyContractV4(TypedDict):
|
||||
class PropertyContractV5(TypedDict):
|
||||
dictionaryUri: NotRequired[str]
|
||||
activationDateUtc: str
|
||||
code: str
|
||||
@@ -710,16 +710,14 @@ class Client:
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
return self.get(endpoint, params)
|
||||
|
||||
def get_property(self, uri, include_classes=False, language_code="", version: int = 4) -> PropertyContractV4:
|
||||
def get_property(self, uri, language_code="", version: int = 5) -> PropertyContractV5:
|
||||
"""
|
||||
Get Property Detail
|
||||
this API replaces Property
|
||||
Get Property details.
|
||||
If you also need the list of classes using the property, then use api/Property/Classes
|
||||
"""
|
||||
|
||||
endpoint = f"Property/v{version}"
|
||||
params = {
|
||||
"uri": uri,
|
||||
"includeClasses": include_classes,
|
||||
"LanguageCode": language_code,
|
||||
}
|
||||
return self.get(endpoint, params)
|
||||
|
||||
@@ -14,6 +14,27 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
template <typename T, typename Cmp = std::less<T>>
|
||||
bool has_intersection(const std::set<T, Cmp>& A,
|
||||
const std::set<T, Cmp>& B) {
|
||||
auto itA = A.begin();
|
||||
auto itB = B.begin();
|
||||
|
||||
while (itA != A.end() && itB != B.end()) {
|
||||
if (Cmp()(*itA, *itB)) {
|
||||
++itA;
|
||||
} else if (Cmp()(*itB, *itA)) {
|
||||
++itB;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, const IfcUtil::IfcBaseClass* inst, const taxonomy::function_item::ptr& fn, std::vector<cross_section>& cross_sections)
|
||||
{
|
||||
std::sort(cross_sections.begin(), cross_sections.end());
|
||||
@@ -25,7 +46,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
||||
// @todo currently only the case is handled where directrix returns a function_item
|
||||
// @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a function_item function
|
||||
if (fn) {
|
||||
function_item_evaluator evaluator(settings_,fn);
|
||||
function_item_evaluator evaluator(settings_, fn);
|
||||
double start = std::max(0., cross_sections.front().dist_along);
|
||||
double end = std::min(fn->length(), cross_sections.back().dist_along);
|
||||
|
||||
@@ -45,6 +66,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
||||
// parameter is minimum number of steps
|
||||
num_steps = (size_t)std::ceil(param);
|
||||
}
|
||||
auto delta_step = curve_length / num_steps;
|
||||
std::vector<double> longitudes;
|
||||
for (auto& x : cross_sections) {
|
||||
longitudes.push_back(x.dist_along);
|
||||
@@ -52,7 +74,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
||||
longitudes.push_back(std::numeric_limits<double>::infinity());
|
||||
auto profile_index = longitudes.begin();
|
||||
for (size_t i = 0; i <= num_steps; ++i) {
|
||||
auto dist_along = start + curve_length / num_steps * i;
|
||||
auto dist_along = start + delta_step * i;
|
||||
while (dist_along > *(profile_index + 1)) {
|
||||
profile_index++;
|
||||
if (profile_index == longitudes.end()) {
|
||||
@@ -60,6 +82,8 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
||||
}
|
||||
}
|
||||
|
||||
const bool is_last_placement_of_this_profile = profile_index + 1 >= longitudes.end() ? false : ((start + delta_step * (i+1)) > *(profile_index + 1));
|
||||
|
||||
auto relative_dist_along = (dist_along - *profile_index) / (*(profile_index + 1) - *profile_index);
|
||||
const auto& profile_a = cross_sections[std::distance(longitudes.begin(), profile_index)].section_geometry;
|
||||
const auto& offset_a = cross_sections[std::distance(longitudes.begin(), profile_index)].offset;
|
||||
@@ -143,28 +167,203 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
||||
} else if (rotation_a != rotation_b) {
|
||||
Logger::Error("Direction vectors on cross section placements only supported when used consistently");
|
||||
}
|
||||
|
||||
taxonomy::loop::ptr w1, w2;
|
||||
taxonomy::edge::ptr e1, e2;
|
||||
taxonomy::point3::ptr p1, p2;
|
||||
|
||||
for (auto tmp_ : boost::combine(loops_a, loops_b)) {
|
||||
boost::tie(w1, w2) = tmp_;
|
||||
if (w1->children.size() != w2->children.size()) {
|
||||
Logger::Warning("Mismatching number of edges: " +
|
||||
std::to_string(w1->children.size()) + " vs " +
|
||||
std::to_string(w2->children.size()),
|
||||
inst
|
||||
);
|
||||
return nullptr;
|
||||
}
|
||||
std::vector<taxonomy::point3::ptr> points;
|
||||
for (auto tmp__ : boost::combine(w1->children, w2->children)) {
|
||||
boost::tie(e1, e2) = tmp__;
|
||||
auto& p1 = boost::get<taxonomy::point3::ptr>(e1->start);
|
||||
auto& p2 = boost::get<taxonomy::point3::ptr>(e2->start);
|
||||
|
||||
auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval();
|
||||
// auto p4 = (interpolated_rotation * p3).eval();
|
||||
points.push_back(taxonomy::make<taxonomy::point3>(p3));
|
||||
if (w1->closed != w2->closed) {
|
||||
Logger::Warning("Mismatching closed property on loops", inst);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (w1->tags.is_initialized() != w2->tags.is_initialized()) {
|
||||
Logger::Warning("Mismatching availability tags on loops", inst);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (w1->tags) {
|
||||
// check uniqueness
|
||||
std::set<std::string> tags_seen;
|
||||
for (const auto& t : *w1->tags) {
|
||||
if (tags_seen.find(t) != tags_seen.end()) {
|
||||
Logger::Warning("Duplicate tag '" + t + "' on loft profile", inst);
|
||||
return nullptr;
|
||||
}
|
||||
tags_seen.insert(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (w2->tags) {
|
||||
// check uniqueness
|
||||
std::set<std::string> tags_seen;
|
||||
for (const auto& t : *w2->tags) {
|
||||
if (tags_seen.find(t) != tags_seen.end()) {
|
||||
Logger::Warning("Duplicate tag '" + t + "' on loft profile", inst);
|
||||
return nullptr;
|
||||
}
|
||||
tags_seen.insert(t);
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, taxonomy::point3::ptr> tag_to_point_on_w1, tag_to_point_on_w2;
|
||||
|
||||
auto loop_to_points = [](const taxonomy::loop::ptr& loop, const boost::optional<std::vector<std::string>>& input_tags) -> std::pair<std::vector<taxonomy::point3::ptr>, std::vector<std::set<std::string>>> {
|
||||
std::vector<taxonomy::point3::ptr> points;
|
||||
std::vector<std::set<std::string>> tags;
|
||||
std::vector<std::string>::const_iterator tag_it;
|
||||
|
||||
if (!loop->closed.get_value_or(false)) {
|
||||
points = {boost::get<taxonomy::point3::ptr>(loop->children[0]->start)};
|
||||
if (input_tags) {
|
||||
tags = {{input_tags->front()}};
|
||||
tag_it = ++input_tags->begin();
|
||||
}
|
||||
}
|
||||
for (auto& e : loop->children) {
|
||||
const auto& p1_ = boost::get<taxonomy::point3::ptr>(e->start);
|
||||
const auto& p2_ = boost::get<taxonomy::point3::ptr>(e->end);
|
||||
if (input_tags && p1_->ccomponents() == p2_->ccomponents()) {
|
||||
tags.back().insert(*tag_it);
|
||||
++tag_it;
|
||||
} else {
|
||||
points.push_back(p2_);
|
||||
if (input_tags) {
|
||||
tags.emplace_back();
|
||||
tags.back().insert(*tag_it);
|
||||
++tag_it;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!input_tags) {
|
||||
if (loop->closed.get_value_or(false)) {
|
||||
// close polygon by referencing first point
|
||||
points.push_back(points.front());
|
||||
}
|
||||
}
|
||||
return {points, tags};
|
||||
};
|
||||
|
||||
auto combine_tags = [](const std::vector<std::set<std::string>>& tag_sets) -> std::set<std::string> {
|
||||
return std::accumulate(
|
||||
tag_sets.begin(), tag_sets.end(), std::set<std::string>{},
|
||||
[](std::set<std::string> acc,
|
||||
const std::set<std::string>& m) {
|
||||
acc.insert(m.begin(), m.end());
|
||||
return acc;
|
||||
});
|
||||
};
|
||||
|
||||
auto join_tags = [](const std::set<std::string>& tag_set) -> std::string {
|
||||
std::string result;
|
||||
for (auto it = tag_set.begin(); it != tag_set.end(); ++it) {
|
||||
if (it != tag_set.begin()) {
|
||||
result += ", ";
|
||||
}
|
||||
result += *it;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
auto [w1_points, w1_tags] = loop_to_points(w1, w1->tags);
|
||||
auto [w2_points, w2_tags] = loop_to_points(w2, w2->tags);
|
||||
|
||||
if (w1->tags && w2->tags) {
|
||||
{
|
||||
auto it = w1_points.begin();
|
||||
auto jt = w1_tags.begin();
|
||||
while (it != w1_points.end() && jt != w1_tags.end()) {
|
||||
for (auto& t : *jt) {
|
||||
tag_to_point_on_w1[t] = *it;
|
||||
}
|
||||
++it;
|
||||
++jt;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto it = w2_points.begin();
|
||||
auto jt = w2_tags.begin();
|
||||
while (it != w2_points.end() && jt != w2_tags.end()) {
|
||||
for (auto& t : *jt) {
|
||||
tag_to_point_on_w2[t] = *it;
|
||||
}
|
||||
++it;
|
||||
++jt;
|
||||
}
|
||||
}
|
||||
|
||||
auto w1_tags_combined = combine_tags(w1_tags);
|
||||
auto w2_tags_combined = combine_tags(w2_tags);
|
||||
|
||||
// For every point (which can have multiple tags in case of 0-width edges) there needs to be a corresponding point on the other profile
|
||||
|
||||
for (auto& p1_tags : w1_tags) {
|
||||
if (!has_intersection(p1_tags, w2_tags_combined)) {
|
||||
Logger::Warning("No matching tags found on loft profiles: " + join_tags(p1_tags) + " not in " + join_tags(w2_tags_combined), inst);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& p2_tags : w2_tags) {
|
||||
if (!has_intersection(p2_tags, w1_tags_combined)) {
|
||||
Logger::Warning("No matching tags found on loft profiles: " + join_tags(p2_tags) + " not in " + join_tags(w1_tags_combined), inst);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (w1->children.size() != w2->children.size()) {
|
||||
Logger::Warning("Mismatching number of edges: " +
|
||||
std::to_string(w1->children.size()) + " vs " +
|
||||
std::to_string(w2->children.size()),
|
||||
inst);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<taxonomy::point3::ptr> points;
|
||||
|
||||
std::vector<std::string> common_tags_vec;
|
||||
if (w1->tags) {
|
||||
std::set<std::string> common_tags;
|
||||
for (const auto& t : *w1->tags) {
|
||||
if (tag_to_point_on_w2.find(t) == tag_to_point_on_w2.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& p1_ = tag_to_point_on_w1[t];
|
||||
const auto& p2_ = tag_to_point_on_w2[t];
|
||||
|
||||
auto p3 = (lerp(p1_->ccomponents(), p2_->ccomponents(), relative_dist_along) + interpolated_offset).eval();
|
||||
|
||||
std::set<std::string> tags_for_this_point_on_subsequent_profile = {t};
|
||||
|
||||
if (is_last_placement_of_this_profile) {
|
||||
for (auto& ts : w2_tags) {
|
||||
if (ts.find(t) != ts.end()) {
|
||||
tags_for_this_point_on_subsequent_profile = ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& x : tags_for_this_point_on_subsequent_profile) {
|
||||
points.push_back(taxonomy::make<taxonomy::point3>(p3));
|
||||
common_tags_vec.push_back(x);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto tmp__ : boost::combine(w1_points, w2_points)) {
|
||||
boost::tie(p1, p2) = tmp__;
|
||||
auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval();
|
||||
points.push_back(taxonomy::make<taxonomy::point3>(p3));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// This is handled in the loop_to_points() function above
|
||||
if (!points.empty()) {
|
||||
if (!w1->closed.get_value_or(true) && !w2->closed.get_value_or(true)) {
|
||||
// open polygon, add last point
|
||||
@@ -178,12 +377,17 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
||||
points.push_back(points.front());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
auto interpolated_loop = polygon_from_points(points);
|
||||
interpolated_loop->external = w1->external;
|
||||
if (interpolated->kind() == taxonomy::FACE) {
|
||||
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
|
||||
interpolated_loop->external = w1->external;
|
||||
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
|
||||
} else {
|
||||
if (w1->tags) {
|
||||
std::static_pointer_cast<taxonomy::loop>(interpolated)->tags = common_tags_vec;
|
||||
}
|
||||
std::static_pointer_cast<taxonomy::loop>(interpolated)->closed = w1->closed;
|
||||
std::static_pointer_cast<taxonomy::loop>(interpolated)->children = interpolated_loop->children;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,34 @@
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <BRepOffsetAPI_ThruSections.hxx>
|
||||
#include <BRepBuilderAPI_MakeSolid.hxx>
|
||||
#include <BRepBuilderAPI_Transform.hxx>
|
||||
|
||||
using namespace ifcopenshell::geometry;
|
||||
using namespace ifcopenshell::geometry::kernels;
|
||||
using namespace IfcGeom;
|
||||
using namespace IfcGeom::util;
|
||||
|
||||
// @todo duplicated
|
||||
namespace {
|
||||
template <typename T, typename Cmp = std::less<T>>
|
||||
bool has_intersection(const std::set<T, Cmp>& A,
|
||||
const std::set<T, Cmp>& B) {
|
||||
auto itA = A.begin();
|
||||
auto itB = B.begin();
|
||||
|
||||
while (itA != A.end() && itB != B.end()) {
|
||||
if (Cmp()(*itA, *itB)) {
|
||||
++itA;
|
||||
} else if (Cmp()(*itB, *itA)) {
|
||||
++itB;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& result) {
|
||||
if (loft->children.size() < 2) {
|
||||
return false;
|
||||
@@ -110,37 +132,125 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
||||
BRep_Builder BB;
|
||||
BB.MakeCompound(comp);
|
||||
|
||||
// @todo this approach is
|
||||
// potentially incorrect as there is no guarantee that the wires for
|
||||
// subsequently placed profiles are traversed from an equivalent start vertex.
|
||||
std::vector<TopoDS_Shape> shps(loft->children.size());
|
||||
std::vector<std::vector<std::set<std::string>>> all_tags;
|
||||
|
||||
for (auto it = loft->children.begin(); it < loft->children.end() - 1; ++it) {
|
||||
|
||||
std::ostringstream oss;
|
||||
loft->children[0]->print(oss);
|
||||
loft->children[1]->print(oss);
|
||||
auto s = oss.str();
|
||||
std::wcout << s.c_str() << std::endl;
|
||||
|
||||
// First convert all taxonomy items to TopoDS_Wire/Face
|
||||
for (auto it = loft->children.begin(); it < loft->children.end(); ++it) {
|
||||
auto i = std::distance(loft->children.begin(), it);
|
||||
if ((*it)->kind() == taxonomy::FACE) {
|
||||
if (!convert(std::static_pointer_cast<taxonomy::face>((*it)), shps[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ((*it)->kind() == taxonomy::LOOP) {
|
||||
|
||||
// @todo duplicated with infra_sweep_helper
|
||||
// I think make_loft() where should just return a shell instead, because
|
||||
// this faceted lofting does not depend on any functionality in the geometry library
|
||||
// and the branching with tags needs to be solved twice otherwise
|
||||
auto loop_to_points = [](const taxonomy::loop::ptr& loop, const boost::optional<std::vector<std::string>>& input_tags) -> std::pair<std::vector<taxonomy::point3::ptr>, std::vector<std::set<std::string>>> {
|
||||
std::vector<taxonomy::point3::ptr> points;
|
||||
std::vector<std::set<std::string>> tags;
|
||||
std::vector<std::string>::const_iterator tag_it;
|
||||
|
||||
if (!loop->closed.get_value_or(false)) {
|
||||
points = {boost::get<taxonomy::point3::ptr>(loop->children[0]->start)};
|
||||
if (input_tags) {
|
||||
tags = {{input_tags->front()}};
|
||||
tag_it = ++input_tags->begin();
|
||||
}
|
||||
}
|
||||
for (auto& e : loop->children) {
|
||||
const auto& p1 = boost::get<taxonomy::point3::ptr>(e->start);
|
||||
const auto& p2 = boost::get<taxonomy::point3::ptr>(e->end);
|
||||
if (input_tags && p1->ccomponents() == p2->ccomponents()) {
|
||||
tags.back().insert(*tag_it);
|
||||
++tag_it;
|
||||
} else {
|
||||
points.push_back(p2);
|
||||
if (input_tags) {
|
||||
tags.emplace_back();
|
||||
tags.back().insert(*tag_it);
|
||||
++tag_it;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!input_tags) {
|
||||
if (loop->closed.get_value_or(false)) {
|
||||
// close polygon by referencing first point
|
||||
points.push_back(points.front());
|
||||
}
|
||||
}
|
||||
return {points, tags};
|
||||
};
|
||||
|
||||
auto lp = std::static_pointer_cast<taxonomy::loop>(*it);
|
||||
TopoDS_Wire w;
|
||||
|
||||
if (lp->tags) {
|
||||
auto [points, tags] = loop_to_points(lp, lp->tags);
|
||||
BRepBuilderAPI_MakePolygon mp;
|
||||
for (auto& p : points) {
|
||||
const auto& xyz = p->ccomponents();
|
||||
mp.Add(gp_Pnt(xyz(0), xyz(1), xyz(2)));
|
||||
}
|
||||
w = mp.Wire();
|
||||
|
||||
if (lp->matrix && !lp->matrix->is_identity()) {
|
||||
const auto& m = lp->matrix->ccomponents();
|
||||
gp_Trsf tr;
|
||||
tr.SetValues(
|
||||
m(0, 0), m(0, 1), m(0, 2), m(0, 3), m(1, 0), m(1, 1), m(1, 2), m(1, 3), m(2, 0), m(2, 1), m(2, 2), m(2, 3));
|
||||
w = TopoDS::Wire(BRepBuilderAPI_Transform(w, tr).Shape());
|
||||
}
|
||||
|
||||
all_tags.push_back(tags);
|
||||
} else {
|
||||
if (!convert(std::static_pointer_cast<taxonomy::loop>((*it)), w)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
shps[i] = w;
|
||||
}
|
||||
if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// With --dimensionality CURVES_SURFACES_AND_SOLIDS this will give the interpolated profiles as line geometry
|
||||
{
|
||||
for (auto& f : shps) {
|
||||
BB.Add(comp, f);
|
||||
}
|
||||
}
|
||||
result = comp;
|
||||
return true;
|
||||
*/
|
||||
|
||||
// @todo this approach is
|
||||
// potentially incorrect as there is no guarantee that the wires for
|
||||
// subsequently placed profiles are traversed from an equivalent start vertex.
|
||||
for (auto it = shps.begin(); it < shps.end() - 1; ++it) {
|
||||
auto ii = std::distance(shps.begin(), it);
|
||||
auto jt = it + 1;
|
||||
std::array<taxonomy::item::ptr, 2> fa = { *it, *jt };
|
||||
std::array<TopoDS_Shape, 2> shps;
|
||||
std::array<std::vector<TopoDS_Shape>::const_iterator, 2> fa = { it, jt };
|
||||
std::vector<std::array<TopoDS_Wire, 2>> ws;
|
||||
ws.emplace_back();
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
if (fa[i]->kind() == taxonomy::FACE) {
|
||||
if (!convert(std::static_pointer_cast<taxonomy::face>(fa[i]), shps[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (fa[i]->kind() == taxonomy::LOOP) {
|
||||
TopoDS_Wire w;
|
||||
if (!convert(std::static_pointer_cast<taxonomy::loop>(fa[i]), w)) {
|
||||
return false;
|
||||
}
|
||||
shps[i] = w;
|
||||
}
|
||||
if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shps[i].ShapeType() == TopAbs_FACE) {
|
||||
ws[0][i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
|
||||
if (fa[i]->ShapeType() == TopAbs_FACE) {
|
||||
ws[0][i] = BRepTools::OuterWire(TopoDS::Face(*fa[i]));
|
||||
size_t j = 1;
|
||||
for (TopExp_Explorer exp(shps[i], TopAbs_WIRE); exp.More(); exp.Next()) {
|
||||
for (TopExp_Explorer exp(*fa[i], TopAbs_WIRE); exp.More(); exp.Next()) {
|
||||
if (exp.Current() != ws[0][i]) {
|
||||
while (ws.size() <= j) {
|
||||
ws.emplace_back();
|
||||
@@ -149,22 +259,110 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ws[0][i] = TopoDS::Wire(shps[i]);
|
||||
ws[0][i] = TopoDS::Wire(*fa[i]);
|
||||
}
|
||||
}
|
||||
if (shps[0].ShapeType() == TopAbs_FACE) {
|
||||
if (it->ShapeType() == TopAbs_FACE) {
|
||||
// When processing a sectioned *surface* there are no
|
||||
// begin and end caps that need to be added.
|
||||
if (it == loft->children.begin()) {
|
||||
if (it == shps.begin()) {
|
||||
// faces.Append(shps[0]);
|
||||
BB.Add(comp, shps[0]);
|
||||
}
|
||||
if (jt == loft->children.end() - 1) {
|
||||
if (jt == shps.end() - 1) {
|
||||
// faces.Append(shps[1]);
|
||||
BB.Add(comp, shps[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!all_tags.empty()) {
|
||||
// only open profiles have tags for now, so there is only one wire, no inner wires
|
||||
const auto& wp = ws[0];
|
||||
std::array<std::vector<gp_Pnt>, 2> profile_points;
|
||||
std::array<std::vector<std::vector<std::set<std::string>>>::const_iterator, 2> tag_pairs = {
|
||||
all_tags.begin() + std::distance(shps.begin(), it),
|
||||
all_tags.begin() + std::distance(shps.begin(), jt)};
|
||||
|
||||
for (size_t i = 0; i < 2; ++i) {
|
||||
TopTools_IndexedDataMapOfShapeListOfShape ancestors;
|
||||
const auto& wire = wp[i];
|
||||
auto& result = profile_points[i];
|
||||
|
||||
TopExp::MapShapesAndAncestors(
|
||||
wire,
|
||||
TopAbs_VERTEX,
|
||||
TopAbs_EDGE,
|
||||
ancestors);
|
||||
|
||||
TopoDS_Vertex v0, vn, previous;
|
||||
TopExp::Vertices(wire, v0, vn);
|
||||
|
||||
TopoDS_Vertex curr = v0;
|
||||
result.push_back(BRep_Tool::Pnt(curr));
|
||||
|
||||
while (true) {
|
||||
if (curr.IsSame(vn)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const TopTools_ListOfShape& incidentEdges = ancestors.FindFromKey(curr);
|
||||
|
||||
for (TopTools_ListIteratorOfListOfShape it(incidentEdges); it.More(); it.Next()) {
|
||||
const TopoDS_Edge& e = TopoDS::Edge(it.Value());
|
||||
|
||||
TopoDS_Vertex ev0, ev1;
|
||||
TopExp::Vertices(e, ev0, ev1);
|
||||
|
||||
TopoDS_Vertex other_on_edge = curr.IsSame(ev0) ? ev1 : ev0;
|
||||
if (other_on_edge.IsSame(previous)) {
|
||||
continue;
|
||||
} else {
|
||||
previous = curr;
|
||||
curr = other_on_edge;
|
||||
result.push_back(BRep_Tool::Pnt(curr));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto a = profile_points[0].begin();
|
||||
auto b = profile_points[1].begin();
|
||||
auto c = tag_pairs[0]->begin();
|
||||
auto d = tag_pairs[1]->begin();
|
||||
|
||||
if (!has_intersection(*c, *d)) {
|
||||
throw std::runtime_error("Starting vertices do not have corresponding tags");
|
||||
}
|
||||
|
||||
auto emit_triangle = [&](const gp_Pnt& p1, const gp_Pnt& p2, const gp_Pnt& p3) {
|
||||
BB.Add(comp, BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(p1, p2, p3, true).Wire()).Face());
|
||||
};
|
||||
|
||||
while (c != (tag_pairs[0]->end() - 1) && d != (tag_pairs[0]->end() - 1)) {
|
||||
if (c != (tag_pairs[0]->end() - 1) && has_intersection(*(c + 1), *d)) {
|
||||
emit_triangle(*a, *(a + 1), *b);
|
||||
++a;
|
||||
++c;
|
||||
} else if (d != (tag_pairs[1]->end() - 1) && has_intersection(*c, *(d + 1))) {
|
||||
emit_triangle(*a, *(b + 1), *b);
|
||||
++b;
|
||||
++d;
|
||||
} else if (c != (tag_pairs[0]->end() - 1) && d != (tag_pairs[1]->end() - 1) && has_intersection(*(c + 1), *(d + 1))) {
|
||||
emit_triangle(*a, *(a + 1), *b);
|
||||
emit_triangle(*(a + 1), *(b + 1), *b);
|
||||
++a;
|
||||
++b;
|
||||
++c;
|
||||
++d;
|
||||
} else {
|
||||
throw std::runtime_error("Unable to construct surface");
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& wp : ws) {
|
||||
BRepTools_WireExplorer a(wp[0]);
|
||||
BRepTools_WireExplorer b(wp[1]);
|
||||
|
||||
@@ -50,7 +50,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOpenCrossProfileDef* inst) {
|
||||
if (tags.has_value() && !tags.get().empty()) {
|
||||
tag = tags.get()[0];
|
||||
}
|
||||
// start->tag = tag;
|
||||
|
||||
auto widths = inst->Widths();
|
||||
auto angles = inst->Slopes(); // these are actually angles, but the attribute is called Slopes
|
||||
@@ -79,16 +78,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOpenCrossProfileDef* inst) {
|
||||
tag = tags.get()[i+1];
|
||||
}
|
||||
|
||||
// points.push_back(taxonomy::make<taxonomy::point3>(x, y, z, tag));
|
||||
points.push_back(taxonomy::make<taxonomy::point3>(x, y, z));
|
||||
}
|
||||
|
||||
auto mapped = polygon_from_points(points);
|
||||
if (mapped->kind() == taxonomy::LOOP) {
|
||||
auto r = taxonomy::loop::ptr((taxonomy::loop*)mapped->clone_());
|
||||
r->closed = false;
|
||||
return r;
|
||||
}
|
||||
mapped->closed = false;
|
||||
mapped->tags = tags;
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
|
||||
@@ -822,11 +822,11 @@ namespace {
|
||||
|
||||
|
||||
boost::optional<function_item::ptr> ifcopenshell::geometry::taxonomy::loop_to_function_item_upgrade_impl(ptr item) {
|
||||
boost::optional<function_item::ptr> fi_;
|
||||
boost::optional<function_item::ptr> function_item_;
|
||||
auto loop_ = dcast<loop>(item);
|
||||
if (loop_) {
|
||||
if (loop_->fi.is_initialized()) {
|
||||
fi_ = loop_->fi;
|
||||
if (loop_->function_item.is_initialized()) {
|
||||
function_item_ = loop_->function_item;
|
||||
} else {
|
||||
// piecewise_function is a specialization of function_item - callers don't need to know this detail
|
||||
piecewise_function::spans_t spans;
|
||||
@@ -880,9 +880,9 @@ boost::optional<function_item::ptr> ifcopenshell::geometry::taxonomy::loop_to_fu
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
fi_ = make<piecewise_function>(0.0,spans);
|
||||
loop_->fi = fi_;
|
||||
function_item_ = make<piecewise_function>(0.0, spans);
|
||||
loop_->function_item = function_item_;
|
||||
}
|
||||
}
|
||||
return fi_;
|
||||
return function_item_;
|
||||
}
|
||||
|
||||
@@ -928,7 +928,8 @@ typedef item const* ptr;
|
||||
DECLARE_PTR(loop)
|
||||
|
||||
boost::optional<bool> external, closed;
|
||||
boost::optional<taxonomy::function_item::ptr> fi;
|
||||
boost::optional<taxonomy::function_item::ptr> function_item;
|
||||
boost::optional<std::vector<std::string>> tags;
|
||||
|
||||
bool is_polyhedron() const {
|
||||
for (auto& e : children) {
|
||||
|
||||
@@ -100,10 +100,15 @@ class Usecase:
|
||||
size = self.convert_si_to_unit(1)
|
||||
points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0))
|
||||
if self.polyline:
|
||||
points = [
|
||||
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
|
||||
for p in self.polyline
|
||||
]
|
||||
# Only scale polyline if we have actual slope
|
||||
if self.x_angle and abs(self.x_angle) > 1e-6:
|
||||
points = [
|
||||
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
|
||||
for p in self.polyline
|
||||
]
|
||||
else:
|
||||
points = [(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1])) for p in self.polyline]
|
||||
|
||||
if self.file.schema == "IFC2X3":
|
||||
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
|
||||
else:
|
||||
@@ -114,21 +119,23 @@ class Usecase:
|
||||
else:
|
||||
direction_ratios = (0.0, 0.0, 1.0)
|
||||
|
||||
offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative
|
||||
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
||||
if self.direction_sense == "NEGATIVE":
|
||||
direction_ratios = tuple(-n for n in direction_ratios)
|
||||
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
||||
|
||||
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(self.x_angle))
|
||||
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(self.x_angle))
|
||||
# Calculate depth based on extrusion angle
|
||||
extrusion_angle = abs(self.x_angle) if self.x_angle else 0
|
||||
if extrusion_angle > 1e-6:
|
||||
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(extrusion_angle))
|
||||
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(extrusion_angle))
|
||||
else:
|
||||
perpendicular_depth = self.convert_si_to_unit(self.depth)
|
||||
perpendicular_offset = self.convert_si_to_unit(self.offset)
|
||||
|
||||
position = None
|
||||
# default position for IFC2X3 where .Position is not optional
|
||||
if self.file.schema == "IFC2X3" or self.offset != 0:
|
||||
position_vector = (
|
||||
offset_direction[0] * perpendicular_offset,
|
||||
offset_direction[1] * perpendicular_offset,
|
||||
offset_direction[2] * perpendicular_offset,
|
||||
direction_ratios[0] * perpendicular_offset,
|
||||
direction_ratios[1] * perpendicular_offset,
|
||||
direction_ratios[2] * perpendicular_offset,
|
||||
)
|
||||
position = self.file.createIfcAxis2Placement3D(
|
||||
self.file.createIfcCartesianPoint(position_vector),
|
||||
|
||||
@@ -85,7 +85,6 @@ class Usecase:
|
||||
def create_item(self) -> ifcopenshell.entity_instance:
|
||||
length = self.convert_si_to_unit(self.settings["length"])
|
||||
thickness = self.convert_si_to_unit(self.settings["thickness"])
|
||||
thickness *= 1 / cos(self.settings["x_angle"])
|
||||
if self.settings["direction_sense"] == "NEGATIVE":
|
||||
thickness *= -1
|
||||
points = (
|
||||
@@ -113,7 +112,7 @@ class Usecase:
|
||||
self.file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
),
|
||||
extrusion_direction,
|
||||
self.convert_si_to_unit(self.settings["height"]) * abs(1 / cos(self.settings["x_angle"])),
|
||||
self.convert_si_to_unit(self.settings["height"]),
|
||||
)
|
||||
if self.settings["booleans"]:
|
||||
extrusion = self.apply_booleans(extrusion)
|
||||
|
||||
@@ -243,6 +243,59 @@ class Usecase:
|
||||
except RuntimeError:
|
||||
return None
|
||||
|
||||
def material_sets_are_equal(self, set1: ifcopenshell.entity_instance, set2: ifcopenshell.entity_instance) -> bool:
|
||||
"""Check if two material sets are structurally equivalent."""
|
||||
if set1.is_a() != set2.is_a():
|
||||
return False
|
||||
|
||||
ifc_class = set1.is_a()
|
||||
|
||||
if ifc_class == "IfcMaterialLayerSet":
|
||||
layers1 = set1.MaterialLayers or []
|
||||
layers2 = set2.MaterialLayers or []
|
||||
if len(layers1) != len(layers2):
|
||||
return False
|
||||
for l1, l2 in zip(layers1, layers2):
|
||||
if (l1.Material is None) != (l2.Material is None):
|
||||
return False
|
||||
if l1.Material and l1.Material.Name != l2.Material.Name:
|
||||
return False
|
||||
if l1.LayerThickness != l2.LayerThickness:
|
||||
return False
|
||||
|
||||
elif ifc_class == "IfcMaterialConstituentSet":
|
||||
constituents1 = set1.MaterialConstituents or []
|
||||
constituents2 = set2.MaterialConstituents or []
|
||||
if len(constituents1) != len(constituents2):
|
||||
return False
|
||||
for c1, c2 in zip(constituents1, constituents2):
|
||||
if (c1.Material is None) != (c2.Material is None):
|
||||
return False
|
||||
if c1.Material and c1.Material.Name != c2.Material.Name:
|
||||
return False
|
||||
if c1.Name != c2.Name:
|
||||
return False
|
||||
|
||||
elif ifc_class == "IfcMaterialProfileSet":
|
||||
profiles1 = set1.MaterialProfiles or []
|
||||
profiles2 = set2.MaterialProfiles or []
|
||||
if len(profiles1) != len(profiles2):
|
||||
return False
|
||||
for p1, p2 in zip(profiles1, profiles2):
|
||||
if (p1.Material is None) != (p2.Material is None):
|
||||
return False
|
||||
if p1.Material and p1.Material.Name != p2.Material.Name:
|
||||
return False
|
||||
if (p1.Profile is None) != (p2.Profile is None):
|
||||
return False
|
||||
if p1.Profile:
|
||||
profile_name1 = getattr(p1.Profile, "ProfileName", None)
|
||||
profile_name2 = getattr(p2.Profile, "ProfileName", None)
|
||||
if profile_name1 != profile_name2:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_existing_element(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Get existing element for a library element.
|
||||
|
||||
@@ -264,13 +317,17 @@ class Usecase:
|
||||
name = element.Name
|
||||
return next((e for e in self.file.by_type("IfcMaterial") if e.Name == name), None)
|
||||
|
||||
elif element in MATERIAL_SETS:
|
||||
elif element.is_a() in MATERIAL_SETS:
|
||||
ifc_class = element.is_a()
|
||||
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
|
||||
material_set_name = getattr(element, name_attr)
|
||||
if material_set_name is None:
|
||||
return
|
||||
return next((e for e in self.file.by_type(ifc_class) if getattr(e, name_attr) == material_set_name), None)
|
||||
for candidate in self.file.by_type(ifc_class):
|
||||
if getattr(candidate, name_attr) == material_set_name:
|
||||
if self.material_sets_are_equal(element, candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
elif element.is_a("IfcProfileDef"):
|
||||
profile_name = element.ProfileName
|
||||
@@ -665,12 +722,11 @@ class Usecase:
|
||||
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
|
||||
material_set_name = getattr(element, name_attr)
|
||||
if material_set_name is not None:
|
||||
existing_material_set = next(
|
||||
(e for e in ifc_file.by_type(ifc_class) if getattr(e, name_attr) == material_set_name), None
|
||||
)
|
||||
if existing_material_set is not None:
|
||||
reuse_identities[element_identity] = existing_material_set
|
||||
return existing_material_set
|
||||
for candidate in ifc_file.by_type(ifc_class):
|
||||
if getattr(candidate, name_attr) == material_set_name:
|
||||
if self.material_sets_are_equal(element, candidate):
|
||||
reuse_identities[element_identity] = candidate
|
||||
return candidate
|
||||
|
||||
elif element.is_a("IfcPresentationStyle"):
|
||||
style_name = element.Name
|
||||
|
||||
@@ -212,7 +212,7 @@ class FormatTransformer(lark.Transformer):
|
||||
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
|
||||
if self.element is None:
|
||||
return "0" # Default value if no element context
|
||||
|
||||
|
||||
query_path = args[0]
|
||||
try:
|
||||
value = get_element_value(self.element, query_path)
|
||||
@@ -399,11 +399,11 @@ class GetElementTransformer(lark.Transformer):
|
||||
|
||||
def format(query: str, element: Optional[ifcopenshell.entity_instance] = None) -> str:
|
||||
"""Format a query string with optional element context for variable substitution.
|
||||
|
||||
|
||||
:param query: Format query string (can include {{variable}} placeholders)
|
||||
:param element: Optional IFC element for variable substitution
|
||||
:return: Formatted string
|
||||
|
||||
|
||||
Example:
|
||||
format("{{z}} / 2", element) # Substitutes element's z value
|
||||
format("imperial_length({{z}} / 2, 4)", element) # Uses z in calculation
|
||||
@@ -1257,4 +1257,4 @@ class FacetTransformer(lark.Transformer):
|
||||
|
||||
if comparison.startswith("!"):
|
||||
return not result
|
||||
return result
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user