mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
black .
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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"}:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user