This commit is contained in:
Andrej730
2026-01-12 16:07:34 +05:00
parent 1f845569a5
commit 153de70a71
28 changed files with 352 additions and 320 deletions
+1
View File
@@ -1295,6 +1295,7 @@ class IfcImporter:
if element.is_a("IfcSpace"):
obj.hide_set(True)
class IfcImportSettings:
"""
Initialize only using `IfcImportSettings.factory()`.
@@ -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():
@@ -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
+13 -11
View File
@@ -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:
@@ -260,7 +282,7 @@ class SvgWriter:
paths = self.resource_paths["Stylesheet"]
if not paths:
return
path_list = [p.strip() for p in paths.split(',')]
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}")
+16 -13
View File
@@ -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]
@@ -680,13 +679,13 @@ class BIM_PT_text(Panel):
if len(literal_props.attributes) > 0 and i < len(props.literal_apply_settings):
row = box.row(align=True)
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()
@@ -1208,7 +1208,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# 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"):
@@ -1217,33 +1217,33 @@ class OverrideDuplicateMove(bpy.types.Operator):
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(
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)
@@ -1256,7 +1256,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
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():
@@ -1264,7 +1264,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
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)
@@ -1272,17 +1272,17 @@ class OverrideDuplicateMove(bpy.types.Operator):
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')
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
@@ -1614,7 +1614,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
).id()
# Initialize if not exists
if group not in original_data:
original_data[group] = {}
@@ -1666,20 +1666,22 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
# 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
(
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
@@ -1697,7 +1699,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
if index in group_data:
matching_group_id = group_id
break
if matching_group_id is None:
return
@@ -1709,7 +1711,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
ifc_file.by_id(pset["id"]),
properties={"Aggregate_Index": int(original_data[matching_group_id][index]["Aggregate_Index"])},
)
# 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):
@@ -1722,7 +1724,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
)
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
tool.Collector.assign(tool.Ifc.get_object(part))
assignments = original_data[matching_group_id][index]["Assignment"]
if assignments:
assign_to_annotations(obj, assignments)
@@ -1837,7 +1839,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
@@ -433,29 +433,29 @@ class ObjectMaterialData:
"""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,10 +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(
@@ -625,7 +625,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
obj_material_usage.ReferenceExtent = material.ReferenceExtent
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)
+15 -13
View File
@@ -233,7 +233,7 @@ class BIM_PT_object_material(Panel):
# 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)
@@ -246,7 +246,7 @@ class BIM_PT_object_material(Panel):
"layer": "Material Layers",
"profile": "Material Profiles",
"constituent": "Material Constituents",
"list_item": "Material List Items"
"list_item": "Material List Items",
}
header_text = header_map.get(set_item_name, "Material Items")
self.layout.label(text=header_text)
@@ -255,7 +255,7 @@ class BIM_PT_object_material(Panel):
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)
@@ -268,7 +268,7 @@ class BIM_PT_object_material(Panel):
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_Interior")
@@ -356,7 +356,7 @@ class BIM_PT_object_material(Panel):
# Material Set Information Section
row = self.layout.row(align=True)
box = row.box()
if ObjectMaterialData.data["material_class"] != "IfcMaterialList":
box_row = box.row(align=True)
set_name = ObjectMaterialData.data["set"]["name"]
@@ -395,6 +395,7 @@ class BIM_PT_object_material(Panel):
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
)
@@ -405,10 +406,10 @@ class BIM_PT_object_material(Panel):
# 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()
@@ -416,13 +417,14 @@ class BIM_PT_object_material(Panel):
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
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)
@@ -436,12 +438,12 @@ class BIM_PT_object_material(Panel):
"layer": "Material Layers",
"profile": "Material Profiles",
"constituent": "Material Constituents",
"list_item": "Material List Items"
"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()
@@ -492,11 +494,11 @@ class BIM_PT_object_material(Panel):
if layer_set_direction:
row = self.layout.row(align=True)
row.label(text="BBIM_MaterialLayer Pset")
# 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")
+17 -17
View File
@@ -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
)
+42 -43
View File
@@ -297,13 +297,13 @@ class DumbSlabPlaner:
extrusion = tool.Model.get_extrusion(representation)
if extrusion:
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
# 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))
# FIX: Only apply 1/cos factor when there's actual extrusion slope
if extrusion_angle > 1e-6:
perpendicular_depth = thickness * abs(1 / cos(extrusion_angle))
@@ -311,13 +311,13 @@ class DumbSlabPlaner:
else:
perpendicular_depth = thickness
perpendicular_offset = layer_offset / self.unit_scale
# Check if direction sense needs to be applied
# This should only happen if explicitly requested, not automatically
if layer_params.get("apply_direction_sense", False):
# Store current direction before potential change
old_direction = direction_ratios.copy()
# Apply direction sense logic
existing_x_angle = extrusion_angle
if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or (
@@ -331,26 +331,26 @@ class DumbSlabPlaner:
offset_direction = direction_ratios.copy() * -1
if layer_params["direction_sense"] == "POSITIVE":
direction_ratios *= -1
# If direction changed, update extrusion with rotation compensation
if (direction_ratios.normalized() - old_direction.normalized()).length > 1e-6:
update_extrusion_direction(element, tuple(direction_ratios), obj)
# After updating direction, get the updated extrusion
extrusion = tool.Model.get_extrusion(representation)
# Update depth
extrusion.Depth = perpendicular_depth
# Update position
ifc_position = extrusion.Position
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"):
material.OffsetFromReferenceLine = position.z
if ifc_position:
ifc_position.Location.Coordinates = position
else:
@@ -397,13 +397,12 @@ class DumbSlabPlaner:
representation=representation,
)
def update_extrusion_direction(element: ifcopenshell.entity_instance,
new_direction_ratios: tuple,
obj: bpy.types.Object = None) -> None:
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)
@@ -413,66 +412,66 @@ class DumbSlabPlaner:
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
@@ -481,15 +480,15 @@ class DumbSlabPlaner:
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
@@ -500,7 +499,7 @@ class DumbSlabPlaner:
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)
@@ -778,7 +777,7 @@ class EnableEditingExtrusionProfile(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:
@@ -798,7 +797,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
# 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
@@ -819,12 +818,12 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
# 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')
bpy.ops.object.mode_set(mode="OBJECT")
for vert in obj.data.vertices:
vert.co.y *= scale_factor
else:
@@ -835,7 +834,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
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"}
@@ -858,7 +857,7 @@ class EditExtrusionProfile(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
@@ -895,16 +894,17 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
# 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:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
@@ -953,7 +953,6 @@ 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"
)
+32 -28
View File
@@ -397,28 +397,28 @@ 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)))
# 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,
@@ -426,7 +426,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
obj=obj,
representation=representation,
)
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
@@ -436,7 +436,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
if layer2_objs:
tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"}
@@ -471,51 +471,55 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
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":
depth = extrusion.Depth / abs(1 / cos(existing_x_angle))
perpendicular_depth = depth * abs(1 / cos(x_angle))
# 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":
# 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
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)
@@ -547,11 +551,11 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
offset_direction *= -1
if layer_params["direction_sense"] == "POSITIVE":
final_local_direction *= -1
# 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
@@ -559,19 +563,19 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
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))
# Create and apply rotation matrix
rotation_matrix = Matrix.Rotation(angle, 4, rotation_axis)
obj.matrix_world = rotation_matrix @ obj.matrix_world
@@ -1081,7 +1085,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,7 +399,7 @@ 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()
@@ -408,7 +408,7 @@ class EditItemUI:
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.
@@ -427,7 +427,7 @@ class EditItemUI:
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,7 +1062,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
and not self.is_advanced
):
filepath = self.get_filepath()
# First, load the IFC file temporarily to check for metadata document
temp_ifc = None
has_metadata_doc = False
@@ -1076,7 +1076,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
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"):
@@ -1137,10 +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())
@@ -1776,7 +1776,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
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:
@@ -3165,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"
@@ -3202,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"}
-2
View File
@@ -364,8 +364,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?
+1 -1
View File
@@ -638,7 +638,7 @@ class BIMProperties(PropertyGroup):
],
name="Time Unit",
default="HOUR",
)
)
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
+1 -1
View File
@@ -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})
+5 -5
View File
@@ -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
+6 -6
View File
@@ -33,17 +33,16 @@ 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):
# Restore the instance's CardinalPoint to the new material usage
if instance_cardinal_point is not None:
@@ -51,16 +50,17 @@ def assign_type(
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)
+3 -6
View File
@@ -382,17 +382,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
+16 -16
View File
@@ -1030,16 +1030,16 @@ class Loader(bonsai.core.tool.Loader):
sense_factor = 1
else:
return mesh
if len(layer_set.MaterialLayers) == 1:
return mesh
bm = bmesh.new()
bm.from_mesh(mesh)
prev_co = None
advance_direction = None # Will store direction to advance planes
if not usage:
sense_factor = 1
no = cls.get_extrusion_vector(element).normalized()
@@ -1047,7 +1047,7 @@ class Loader(bonsai.core.tool.Loader):
advance_direction = no
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 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"):
@@ -1057,14 +1057,14 @@ class Loader(bonsai.core.tool.Loader):
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()
# Ensure it points in POSITIVE Y (through wall thickness, not backwards)
if thickness_dir.y < 0:
thickness_dir = -thickness_dir
no = thickness_dir
advance_direction = thickness_dir
elif usage.LayerSetDirection == "AXIS3":
@@ -1077,10 +1077,10 @@ class Loader(bonsai.core.tool.Loader):
no = cls.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
advance_direction = no
no *= sense_factor
advance_direction *= sense_factor
# Cache this
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
styles = {}
@@ -1088,25 +1088,25 @@ class Loader(bonsai.core.tool.Loader):
for i, material in enumerate(mesh.materials):
if style := tool.Ifc.get_entity(material):
styles[style] = i
last_i = len(layer_set.MaterialLayers) - 1
for i, layer in enumerate(layer_set.MaterialLayers):
if i != last_i:
prev_co = co.copy()
# Use advance_direction (not no) to move planes!
co += advance_direction * layer.LayerThickness * cls.unit_scale
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:
for face in bisect_geom["geom"]:
if isinstance(face, bmesh.types.BMFace):
@@ -1139,14 +1139,14 @@ class Loader(bonsai.core.tool.Loader):
item = item.FirstOperand
if item.is_a("IfcExtrudedAreaSolid"):
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])
+14 -13
View File
@@ -620,12 +620,11 @@ 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")
@@ -633,24 +632,24 @@ class Model(bonsai.core.tool.Model):
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 = {
@@ -667,14 +666,14 @@ class Model(bonsai.core.tool.Model):
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":
@@ -718,14 +717,16 @@ class Model(bonsai.core.tool.Model):
)
@classmethod
def get_material_layer_custom_offset(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> Optional[float]:
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)
# 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")
@@ -733,7 +734,7 @@ class Model(bonsai.core.tool.Model):
# 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":
@@ -753,7 +754,7 @@ class Model(bonsai.core.tool.Model):
return None
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"}:
+28 -22
View File
@@ -529,26 +529,34 @@ class Project(bonsai.core.tool.Project):
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
})
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
})
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
@@ -556,14 +564,12 @@ class Project(bonsai.core.tool.Project):
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
})
tool.Ifc.run("document.edit_information", information=doc, attributes={"Location": metadata_filename})
@classmethod
def remove_metadata_document_information(cls) -> None:
+2 -2
View File
@@ -166,11 +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
@@ -1,4 +1,3 @@
import bonsai, json, bpy
from pathlib import Path
@@ -11,14 +10,15 @@ 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(),
})
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")
@@ -107,23 +107,20 @@ class Usecase:
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
]
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:
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points))
if self.x_angle:
direction_ratios = (0.0, sin(self.x_angle), cos(self.x_angle))
else:
direction_ratios = (0.0, 0.0, 1.0)
extrusion_direction = self.file.createIfcDirection(direction_ratios)
# Calculate depth based on extrusion angle
extrusion_angle = abs(self.x_angle) if self.x_angle else 0
if extrusion_angle > 1e-6:
@@ -132,7 +129,7 @@ class Usecase:
else:
perpendicular_depth = self.convert_si_to_unit(self.depth)
perpendicular_offset = self.convert_si_to_unit(self.offset)
position = None
if self.file.schema == "IFC2X3" or self.offset != 0:
position_vector = (
@@ -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