mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 02:07:36 +00:00
black .
This commit is contained in:
@@ -1295,6 +1295,7 @@ class IfcImporter:
|
|||||||
if element.is_a("IfcSpace"):
|
if element.is_a("IfcSpace"):
|
||||||
obj.hide_set(True)
|
obj.hide_set(True)
|
||||||
|
|
||||||
|
|
||||||
class IfcImportSettings:
|
class IfcImportSettings:
|
||||||
"""
|
"""
|
||||||
Initialize only using `IfcImportSettings.factory()`.
|
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"]:
|
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")
|
self.layout.operator("bim.activate_drawing_by_annotation", text="Go to Drawing")
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
if not bpy.app.background:
|
if not bpy.app.background:
|
||||||
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
|
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.load_post.append(handler.load_post)
|
||||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
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_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():
|
def unregister():
|
||||||
|
|||||||
@@ -353,20 +353,20 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
# Clear any local camera setup and force viewport to use scene camera
|
# Clear any local camera setup and force viewport to use scene camera
|
||||||
for area in context.screen.areas:
|
for area in context.screen.areas:
|
||||||
if area.type == 'VIEW_3D':
|
if area.type == "VIEW_3D":
|
||||||
for space in area.spaces:
|
for space in area.spaces:
|
||||||
if space.type == 'VIEW_3D':
|
if space.type == "VIEW_3D":
|
||||||
# Clear local camera to ensure we use scene.camera
|
# Clear local camera to ensure we use scene.camera
|
||||||
space.use_local_camera = False
|
space.use_local_camera = False
|
||||||
space.camera = context.scene.camera
|
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}")
|
print(f"Set viewport camera to: {context.scene.camera.name}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Force complete scene update
|
# Force complete scene update
|
||||||
context.view_layer.update()
|
context.view_layer.update()
|
||||||
context.evaluated_depsgraph_get()
|
context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
underlay_svg = self.generate_underlay(context)
|
underlay_svg = self.generate_underlay(context)
|
||||||
|
|
||||||
with profile("Generate linework"):
|
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"})
|
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
|
||||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||||
filename_ext = ".svg"
|
filename_ext = ".svg"
|
||||||
|
|
||||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)
|
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):
|
def _execute(self, context):
|
||||||
# Handle both single and multiple file selection
|
# 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):
|
for i, literal_backup in enumerate(literals_backup):
|
||||||
if i < len(props.literals):
|
if i < len(props.literals):
|
||||||
literal_props = props.literals[i]
|
literal_props = props.literals[i]
|
||||||
|
|
||||||
if assigned_product_obj:
|
if assigned_product_obj:
|
||||||
literal_props.product_used = assigned_product_obj
|
literal_props.product_used = assigned_product_obj
|
||||||
elif "product_used" in literal_backup and literal_backup["product_used"]:
|
elif "product_used" in literal_backup and literal_backup["product_used"]:
|
||||||
product_name = literal_backup["product_used"]
|
product_name = literal_backup["product_used"]
|
||||||
if product_name in bpy.data.objects:
|
if product_name in bpy.data.objects:
|
||||||
literal_props.product_used = bpy.data.objects[product_name]
|
literal_props.product_used = bpy.data.objects[product_name]
|
||||||
|
|
||||||
literal_props.element_value_rows.clear()
|
literal_props.element_value_rows.clear()
|
||||||
if "element_value_rows" in literal_backup:
|
if "element_value_rows" in literal_backup:
|
||||||
for row_data in literal_backup["element_value_rows"]:
|
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_label = "Activate Drawing"
|
||||||
bl_description = "Activate the drawing corresponding to the selected annotation"
|
bl_description = "Activate the drawing corresponding to the selected annotation"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
# Check if an annotation object is selected
|
# Check if an annotation object is selected
|
||||||
if not context.selected_objects:
|
if not context.selected_objects:
|
||||||
cls.poll_message_set("No object selected")
|
cls.poll_message_set("No object selected")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
active_obj = context.active_object
|
active_obj = context.active_object
|
||||||
if not active_obj:
|
if not active_obj:
|
||||||
cls.poll_message_set("No active object")
|
cls.poll_message_set("No active object")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
element = tool.Ifc.get_entity(active_obj)
|
element = tool.Ifc.get_entity(active_obj)
|
||||||
if not element:
|
if not element:
|
||||||
cls.poll_message_set("Selected object is not an IFC element")
|
cls.poll_message_set("Selected object is not an IFC element")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check if it's an IfcAnnotation with ObjectType = "SECTION" or "ELEVATION"
|
# Check if it's an IfcAnnotation with ObjectType = "SECTION" or "ELEVATION"
|
||||||
if not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "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")
|
cls.poll_message_set("Selected object is not a drawing annotation")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
active_obj = context.active_object
|
active_obj = context.active_object
|
||||||
element = tool.Ifc.get_entity(active_obj)
|
element = tool.Ifc.get_entity(active_obj)
|
||||||
|
|
||||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "ELEVATION"]:
|
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")
|
self.report({"ERROR"}, "Selected object is not a drawing annotation")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
# Find the drawing/camera element that this annotation references
|
# Find the drawing/camera element that this annotation references
|
||||||
drawing_element = self.find_drawing_from_annotation(element)
|
drawing_element = self.find_drawing_from_annotation(element)
|
||||||
|
|
||||||
if not drawing_element:
|
if not drawing_element:
|
||||||
self.report({"ERROR"}, "Could not find drawing element for this annotation")
|
self.report({"ERROR"}, "Could not find drawing element for this annotation")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
# Use the existing ActivateDrawing operator with the drawing element's ID
|
# Use the existing ActivateDrawing operator with the drawing element's ID
|
||||||
bpy.ops.bim.activate_drawing(drawing=drawing_element.id())
|
bpy.ops.bim.activate_drawing(drawing=drawing_element.id())
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def find_drawing_from_annotation(self, annotation_element):
|
def find_drawing_from_annotation(self, annotation_element):
|
||||||
"""Find the drawing/camera element that this annotation references."""
|
"""Find the drawing/camera element that this annotation references."""
|
||||||
ifc = tool.Ifc.get()
|
ifc = tool.Ifc.get()
|
||||||
|
|
||||||
# Check IfcRelAssignsToProduct relationships
|
# Check IfcRelAssignsToProduct relationships
|
||||||
for rel in ifc.get_inverse(annotation_element):
|
for rel in ifc.get_inverse(annotation_element):
|
||||||
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct:
|
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct:
|
||||||
if rel.RelatingProduct.is_a("IfcAnnotation"):
|
if rel.RelatingProduct.is_a("IfcAnnotation"):
|
||||||
# Found the drawing element!
|
# Found the drawing element!
|
||||||
return rel.RelatingProduct
|
return rel.RelatingProduct
|
||||||
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -5062,7 +5061,7 @@ class AddElementValueRow(bpy.types.Operator):
|
|||||||
new_row.category = literal_props.category_for_adding
|
new_row.category = literal_props.category_for_adding
|
||||||
new_row.element_key = ""
|
new_row.element_key = ""
|
||||||
new_row.formatted_value = ""
|
new_row.formatted_value = ""
|
||||||
|
|
||||||
if len(literal_props.element_value_rows) == 1:
|
if len(literal_props.element_value_rows) == 1:
|
||||||
new_row.separator = ""
|
new_row.separator = ""
|
||||||
else:
|
else:
|
||||||
@@ -5106,10 +5105,10 @@ class ElementValueSuggestionsPopup(bpy.types.Operator):
|
|||||||
row_index: bpy.props.IntProperty()
|
row_index: bpy.props.IntProperty()
|
||||||
category: bpy.props.StringProperty()
|
category: bpy.props.StringProperty()
|
||||||
search_query: bpy.props.StringProperty(name="Search", description="Search for element values")
|
search_query: bpy.props.StringProperty(name="Search", description="Search for element values")
|
||||||
|
|
||||||
collection_keys: bpy.props.CollectionProperty(type=StrProperty)
|
collection_keys: bpy.props.CollectionProperty(type=StrProperty)
|
||||||
collection_descriptions: bpy.props.CollectionProperty(type=StrProperty)
|
collection_descriptions: bpy.props.CollectionProperty(type=StrProperty)
|
||||||
|
|
||||||
selected_key: bpy.props.StringProperty()
|
selected_key: bpy.props.StringProperty()
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
@@ -5157,13 +5156,13 @@ class ElementValueSuggestionsPopup(bpy.types.Operator):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
layout.prop_search(self, "selected_key", self, "collection_descriptions", text="Value")
|
layout.prop_search(self, "selected_key", self, "collection_descriptions", text="Value")
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
if not self.selected_key:
|
if not self.selected_key:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
if not obj:
|
if not obj:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
@@ -5177,7 +5176,7 @@ class ElementValueSuggestionsPopup(bpy.types.Operator):
|
|||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
value_row = literal_props.element_value_rows[self.row_index]
|
value_row = literal_props.element_value_rows[self.row_index]
|
||||||
|
|
||||||
for idx, desc_item in enumerate(self.collection_descriptions):
|
for idx, desc_item in enumerate(self.collection_descriptions):
|
||||||
if desc_item.name == self.selected_key:
|
if desc_item.name == self.selected_key:
|
||||||
actual_key = self.collection_keys[idx].name
|
actual_key = self.collection_keys[idx].name
|
||||||
@@ -5260,10 +5259,7 @@ class FormatElementValueRow(bpy.types.Operator):
|
|||||||
|
|
||||||
custom_expression: bpy.props.StringProperty(
|
custom_expression: bpy.props.StringProperty(
|
||||||
name="Custom Expression",
|
name="Custom Expression",
|
||||||
description=(
|
description=("Custom expression using functions\n" "Use {{value}} as placeholder for the current row's value."),
|
||||||
"Custom expression using functions\n"
|
|
||||||
"Use {{value}} as placeholder for the current row's value."
|
|
||||||
),
|
|
||||||
default='concat({{value}}, " - additional text")',
|
default='concat({{value}}, " - additional text")',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5288,51 +5284,51 @@ class FormatElementValueRow(bpy.types.Operator):
|
|||||||
def _load_formatting_from_row(self, row):
|
def _load_formatting_from_row(self, row):
|
||||||
"""Parse the formatted_value to load existing formatting settings"""
|
"""Parse the formatted_value to load existing formatting settings"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
formatted_value = row.formatted_value
|
formatted_value = row.formatted_value
|
||||||
|
|
||||||
if not formatted_value or formatted_value == f"{{{{{row.element_key}}}}}":
|
if not formatted_value or formatted_value == f"{{{{{row.element_key}}}}}":
|
||||||
self.formatting_type = "NONE"
|
self.formatting_type = "NONE"
|
||||||
return
|
return
|
||||||
|
|
||||||
if formatted_value.startswith("``") and formatted_value.endswith("``"):
|
if formatted_value.startswith("``") and formatted_value.endswith("``"):
|
||||||
expression = formatted_value[2:-2].strip()
|
expression = formatted_value[2:-2].strip()
|
||||||
else:
|
else:
|
||||||
self.formatting_type = "NONE"
|
self.formatting_type = "NONE"
|
||||||
return
|
return
|
||||||
|
|
||||||
if match := re.match(r"upper\(\{\{[^}]+\}\}\)", expression):
|
if match := re.match(r"upper\(\{\{[^}]+\}\}\)", expression):
|
||||||
self.formatting_type = "UPPER"
|
self.formatting_type = "UPPER"
|
||||||
|
|
||||||
elif match := re.match(r"lower\(\{\{[^}]+\}\}\)", expression):
|
elif match := re.match(r"lower\(\{\{[^}]+\}\}\)", expression):
|
||||||
self.formatting_type = "LOWER"
|
self.formatting_type = "LOWER"
|
||||||
|
|
||||||
elif match := re.match(r"title\(\{\{[^}]+\}\}\)", expression):
|
elif match := re.match(r"title\(\{\{[^}]+\}\}\)", expression):
|
||||||
self.formatting_type = "TITLE"
|
self.formatting_type = "TITLE"
|
||||||
|
|
||||||
elif match := re.match(r"int\(\{\{[^}]+\}\}\)", expression):
|
elif match := re.match(r"int\(\{\{[^}]+\}\}\)", expression):
|
||||||
self.formatting_type = "INT"
|
self.formatting_type = "INT"
|
||||||
|
|
||||||
elif match := re.match(r"round\(\{\{[^}]+\}\},\s*([^)]+)\)", expression):
|
elif match := re.match(r"round\(\{\{[^}]+\}\},\s*([^)]+)\)", expression):
|
||||||
self.formatting_type = "ROUND"
|
self.formatting_type = "ROUND"
|
||||||
self.round_precision = match.group(1).strip()
|
self.round_precision = match.group(1).strip()
|
||||||
|
|
||||||
elif match := re.match(r"number\(\{\{[^}]+\}\},\s*([^,]+),\s*([^)]+)\)", expression):
|
elif match := re.match(r"number\(\{\{[^}]+\}\},\s*([^,]+),\s*([^)]+)\)", expression):
|
||||||
self.formatting_type = "NUMBER"
|
self.formatting_type = "NUMBER"
|
||||||
self.decimal_separator = match.group(1).strip()
|
self.decimal_separator = match.group(1).strip()
|
||||||
self.thousands_separator = match.group(2).strip()
|
self.thousands_separator = match.group(2).strip()
|
||||||
|
|
||||||
elif match := re.match(r"metric_length\(\{\{[^}]+\}\},\s*([^,]+),\s*([^)]+)\)", expression):
|
elif match := re.match(r"metric_length\(\{\{[^}]+\}\},\s*([^,]+),\s*([^)]+)\)", expression):
|
||||||
self.formatting_type = "METRIC_LENGTH"
|
self.formatting_type = "METRIC_LENGTH"
|
||||||
self.metric_precision = match.group(1).strip()
|
self.metric_precision = match.group(1).strip()
|
||||||
self.metric_decimals = int(match.group(2).strip())
|
self.metric_decimals = int(match.group(2).strip())
|
||||||
|
|
||||||
elif match := re.match(r'imperial_length\(\{\{[^}]+\}\},\s*(\d+),\s*"([^"]+)",\s*"([^"]+)"\)', expression):
|
elif match := re.match(r'imperial_length\(\{\{[^}]+\}\},\s*(\d+),\s*"([^"]+)",\s*"([^"]+)"\)', expression):
|
||||||
self.formatting_type = "IMPERIAL_LENGTH"
|
self.formatting_type = "IMPERIAL_LENGTH"
|
||||||
self.imperial_precision = int(match.group(1).strip())
|
self.imperial_precision = int(match.group(1).strip())
|
||||||
self.imperial_input_unit = match.group(2).strip()
|
self.imperial_input_unit = match.group(2).strip()
|
||||||
self.imperial_output_unit = match.group(3).strip()
|
self.imperial_output_unit = match.group(3).strip()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.formatting_type = "CUSTOM"
|
self.formatting_type = "CUSTOM"
|
||||||
self.custom_expression = expression
|
self.custom_expression = expression
|
||||||
@@ -5450,9 +5446,9 @@ class ApplyElementValueRowsToLiteral(bpy.types.Operator):
|
|||||||
default_format = f"{{{{{row.element_key}}}}}"
|
default_format = f"{{{{{row.element_key}}}}}"
|
||||||
row.formatted_value = default_format
|
row.formatted_value = default_format
|
||||||
value_part = default_format
|
value_part = default_format
|
||||||
|
|
||||||
parts.append(row.separator + value_part)
|
parts.append(row.separator + value_part)
|
||||||
|
|
||||||
concatenated_value = "".join(parts)
|
concatenated_value = "".join(parts)
|
||||||
|
|
||||||
for attr in literal_props.attributes:
|
for attr in literal_props.attributes:
|
||||||
@@ -5469,12 +5465,12 @@ class ApplyElementValueRowsToLiteral(bpy.types.Operator):
|
|||||||
This preserves formatting functions like upper(), round(), etc.
|
This preserves formatting functions like upper(), round(), etc.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
pattern = r'\{\{[^}]+\}\}'
|
pattern = r"\{\{[^}]+\}\}"
|
||||||
|
|
||||||
new_base_value = f"{{{{{new_element_key}}}}}"
|
new_base_value = f"{{{{{new_element_key}}}}}"
|
||||||
updated_value = re.sub(pattern, new_base_value, old_formatted_value)
|
updated_value = re.sub(pattern, new_base_value, old_formatted_value)
|
||||||
|
|
||||||
return updated_value
|
return updated_value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -714,7 +714,9 @@ class ElementValueRow(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
element_key: StringProperty(
|
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(
|
formatted_value: StringProperty(
|
||||||
@@ -756,33 +758,33 @@ def get_category_items_with_counts(self, context):
|
|||||||
("Coordinates", "Coordinates", "Coordinate information", "EMPTY_ARROWS"),
|
("Coordinates", "Coordinates", "Coordinate information", "EMPTY_ARROWS"),
|
||||||
("Custom String", "Custom String", "Add custom text (no element key)", "SMALL_CAPS"),
|
("Custom String", "Custom String", "Add custom text (no element key)", "SMALL_CAPS"),
|
||||||
]
|
]
|
||||||
|
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
|
|
||||||
if obj and tool.Ifc.get_entity(obj):
|
if obj and tool.Ifc.get_entity(obj):
|
||||||
try:
|
try:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
text_element = element
|
text_element = element
|
||||||
|
|
||||||
if hasattr(self, 'product_used'):
|
if hasattr(self, "product_used"):
|
||||||
if self.product_used:
|
if self.product_used:
|
||||||
element = tool.Ifc.get_entity(self.product_used)
|
element = tool.Ifc.get_entity(self.product_used)
|
||||||
else:
|
else:
|
||||||
assigned = tool.Drawing.get_assigned_product(text_element)
|
assigned = tool.Drawing.get_assigned_product(text_element)
|
||||||
if assigned:
|
if assigned:
|
||||||
element = assigned
|
element = assigned
|
||||||
|
|
||||||
available_keys = ElementValuesData.get_available_element_value_keys(element)
|
available_keys = ElementValuesData.get_available_element_value_keys(element)
|
||||||
items = []
|
items = []
|
||||||
for i, (identifier, base_name, description, icon) in enumerate(category_metadata):
|
for i, (identifier, base_name, description, icon) in enumerate(category_metadata):
|
||||||
count = len(available_keys.get(identifier, []))
|
count = len(available_keys.get(identifier, []))
|
||||||
display_name = f"{base_name} ({count})" if count > 0 else base_name
|
display_name = f"{base_name} ({count})" if count > 0 else base_name
|
||||||
items.append((identifier, display_name, description, icon, i))
|
items.append((identifier, display_name, description, icon, i))
|
||||||
|
|
||||||
return items
|
return items
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return [(id, name, desc, icon, i) for i, (id, name, desc, icon) in enumerate(category_metadata)]
|
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(
|
element_value_rows: CollectionProperty(
|
||||||
name="Element Value Rows",
|
name="Element Value Rows",
|
||||||
type=ElementValueRow,
|
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(
|
category_for_adding: EnumProperty(
|
||||||
name="Category for Adding",
|
name="Category for Adding",
|
||||||
items=get_category_items_with_counts,
|
items=get_category_items_with_counts,
|
||||||
default=0,
|
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:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -115,9 +115,13 @@ def parse_markdown_it(text: str) -> list[dict[str, Union[str, None]]]:
|
|||||||
if tokens[j].type == "inline":
|
if tokens[j].type == "inline":
|
||||||
for child in tokens[j].children or []:
|
for child in tokens[j].children or []:
|
||||||
if child.type == "softbreak":
|
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>":
|
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":
|
elif child.type == "strong_open":
|
||||||
bold = True
|
bold = True
|
||||||
elif child.type == "strong_close":
|
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:
|
elif child.type == "link_close" and link_opening:
|
||||||
url = link_opening.attrGet("href")
|
url = link_opening.attrGet("href")
|
||||||
if url and link_text:
|
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_opening = None
|
||||||
link_text = None
|
link_text = None
|
||||||
elif child.type == "text" and not link_opening:
|
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
|
j += 1
|
||||||
i = j
|
i = j
|
||||||
else:
|
else:
|
||||||
@@ -168,7 +188,9 @@ def parse_markdown_it(text: str) -> list[dict[str, Union[str, None]]]:
|
|||||||
link_opening = None
|
link_opening = None
|
||||||
link_text = None
|
link_text = None
|
||||||
elif child.type == "text" and not link_opening:
|
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
|
i += 1
|
||||||
segments = [seg for seg in segments if seg.get("text") is not None or seg.get("break", False)]
|
segments = [seg for seg in segments if seg.get("text") is not None or seg.get("break", False)]
|
||||||
if not segments:
|
if not segments:
|
||||||
@@ -260,7 +282,7 @@ class SvgWriter:
|
|||||||
paths = self.resource_paths["Stylesheet"]
|
paths = self.resource_paths["Stylesheet"]
|
||||||
if not paths:
|
if not paths:
|
||||||
return
|
return
|
||||||
path_list = [p.strip() for p in paths.split(',')]
|
path_list = [p.strip() for p in paths.split(",")]
|
||||||
for path in path_list:
|
for path in path_list:
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
print(f"WARNING. Couldn't find stylesheet for the drawing by the path: {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="")
|
op = row3.operator("bim.activate_drawing_from_sheet", icon="OUTLINER_OB_CAMERA", text="")
|
||||||
|
|
||||||
|
|
||||||
if active_sheet.reference_type == "DRAWING":
|
if active_sheet.reference_type == "DRAWING":
|
||||||
drawingnamesvg = active_sheet.name
|
drawingnamesvg = active_sheet.name
|
||||||
drawingname = drawingnamesvg.split(".svg")[0]
|
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):
|
if len(literal_props.attributes) > 0 and i < len(props.literal_apply_settings):
|
||||||
row = box.row(align=True)
|
row = box.row(align=True)
|
||||||
bonsai.bim.helper.draw_attribute(literal_props.attributes[0], row, enable_search=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"
|
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 = row.operator("bim.toggle_element_values_panel", icon=expand_icon, text="")
|
||||||
op.literal_prop_id = i
|
op.literal_prop_id = i
|
||||||
|
|
||||||
row.prop(props.literal_apply_settings[i], "apply_text_to_all", text="", icon="COPYDOWN")
|
row.prop(props.literal_apply_settings[i], "apply_text_to_all", text="", icon="COPYDOWN")
|
||||||
|
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
assigned_element = tool.Drawing.get_assigned_product(element) or element
|
assigned_element = tool.Drawing.get_assigned_product(element) or element
|
||||||
resolved_value = tool.Drawing.replace_text_literal_variables(
|
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")
|
element_values_row.prop(literal_props, "product_used", text="", icon="EYEDROPPER")
|
||||||
|
|
||||||
current_product = get_current_product_for_element_values(obj, literal_props)
|
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 = values_box.row()
|
||||||
source_row.label(text=f"Source: {product_name}", icon="OBJECT_DATA")
|
source_row.label(text=f"Source: {product_name}", icon="OBJECT_DATA")
|
||||||
|
|
||||||
@@ -720,29 +721,29 @@ class BIM_PT_text(Panel):
|
|||||||
if element:
|
if element:
|
||||||
add_row = values_box.row(align=True)
|
add_row = values_box.row(align=True)
|
||||||
add_row.prop(literal_props, "category_for_adding", text="")
|
add_row.prop(literal_props, "category_for_adding", text="")
|
||||||
|
|
||||||
op = add_row.operator("bim.add_element_value_row", text="Add Element", icon="ADD")
|
op = add_row.operator("bim.add_element_value_row", text="Add Element", icon="ADD")
|
||||||
op.literal_prop_id = i
|
op.literal_prop_id = i
|
||||||
|
|
||||||
if len(literal_props.element_value_rows) > 0:
|
if len(literal_props.element_value_rows) > 0:
|
||||||
for row_idx, value_row in enumerate(literal_props.element_value_rows):
|
for row_idx, value_row in enumerate(literal_props.element_value_rows):
|
||||||
row = values_box.row(align=True)
|
row = values_box.row(align=True)
|
||||||
|
|
||||||
is_custom_string = value_row.category == "Custom String"
|
is_custom_string = value_row.category == "Custom String"
|
||||||
|
|
||||||
if is_custom_string:
|
if is_custom_string:
|
||||||
category_icon = get_category_icon(value_row.category)
|
category_icon = get_category_icon(value_row.category)
|
||||||
row.prop(value_row, "element_key", text="", icon=category_icon)
|
row.prop(value_row, "element_key", text="", icon=category_icon)
|
||||||
else:
|
else:
|
||||||
split = row.split(factor=0.25, align=True)
|
split = row.split(factor=0.25, align=True)
|
||||||
|
|
||||||
sep_col = split.row(align=True)
|
sep_col = split.row(align=True)
|
||||||
sep_col.prop(value_row, "separator", text="")
|
sep_col.prop(value_row, "separator", text="")
|
||||||
|
|
||||||
key_col = split.row(align=True)
|
key_col = split.row(align=True)
|
||||||
category_icon = get_category_icon(value_row.category)
|
category_icon = get_category_icon(value_row.category)
|
||||||
key_col.prop(value_row, "element_key", text="", icon=category_icon)
|
key_col.prop(value_row, "element_key", text="", icon=category_icon)
|
||||||
|
|
||||||
op = row.operator("bim.element_value_suggestions_popup", text="", icon="VIEWZOOM")
|
op = row.operator("bim.element_value_suggestions_popup", text="", icon="VIEWZOOM")
|
||||||
op.literal_prop_id = i
|
op.literal_prop_id = i
|
||||||
op.row_index = row_idx
|
op.row_index = row_idx
|
||||||
@@ -758,7 +759,9 @@ class BIM_PT_text(Panel):
|
|||||||
|
|
||||||
apply_row = values_box.row()
|
apply_row = values_box.row()
|
||||||
apply_row.scale_y = 1.2
|
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
|
op.literal_prop_id = i
|
||||||
else:
|
else:
|
||||||
error_row = values_box.row()
|
error_row = values_box.row()
|
||||||
|
|||||||
@@ -1208,7 +1208,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
# Expand selection to include all parts of selected aggregates
|
# Expand selection to include all parts of selected aggregates
|
||||||
objects_to_duplicate = set(context.selected_objects) - objects_to_remove
|
objects_to_duplicate = set(context.selected_objects) - objects_to_remove
|
||||||
expanded_objects = set(objects_to_duplicate)
|
expanded_objects = set(objects_to_duplicate)
|
||||||
|
|
||||||
for obj in objects_to_duplicate:
|
for obj in objects_to_duplicate:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if element and element.is_a("IfcElementAssembly"):
|
if element and element.is_a("IfcElementAssembly"):
|
||||||
@@ -1217,33 +1217,33 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
part_obj = tool.Ifc.get_object(part)
|
part_obj = tool.Ifc.get_object(part)
|
||||||
if part_obj:
|
if part_obj:
|
||||||
expanded_objects.add(part_obj)
|
expanded_objects.add(part_obj)
|
||||||
|
|
||||||
# Store parent aggregate relationships
|
# Store parent aggregate relationships
|
||||||
parent_aggregates = {}
|
parent_aggregates = {}
|
||||||
|
|
||||||
for obj in expanded_objects:
|
for obj in expanded_objects:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if element and element.is_a("IfcElementAssembly"):
|
if element and element.is_a("IfcElementAssembly"):
|
||||||
parent_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
parent_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
if parent_aggregate:
|
if parent_aggregate:
|
||||||
parent_aggregates[element] = parent_aggregate
|
parent_aggregates[element] = parent_aggregate
|
||||||
|
|
||||||
old_to_new, new_active_obj = tool.Geometry.duplicate_ifc_objects(
|
old_to_new, new_active_obj = tool.Geometry.duplicate_ifc_objects(
|
||||||
expanded_objects,
|
expanded_objects,
|
||||||
linked=linked,
|
linked=linked,
|
||||||
active_object=context.active_object,
|
active_object=context.active_object,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Restore parent aggregate relationships, but only for parents that were NOT duplicated
|
# Restore parent aggregate relationships, but only for parents that were NOT duplicated
|
||||||
for old_elem, new_elems in old_to_new.items():
|
for old_elem, new_elems in old_to_new.items():
|
||||||
if old_elem in parent_aggregates:
|
if old_elem in parent_aggregates:
|
||||||
old_parent = parent_aggregates[old_elem]
|
old_parent = parent_aggregates[old_elem]
|
||||||
|
|
||||||
# Check if the parent was also duplicated
|
# Check if the parent was also duplicated
|
||||||
if old_parent in old_to_new:
|
if old_parent in old_to_new:
|
||||||
# The duplication already created the correct nested relationship
|
# The duplication already created the correct nested relationship
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Parent was NOT duplicated, so we need to assign to the original parent
|
# Parent was NOT duplicated, so we need to assign to the original parent
|
||||||
for new_elem in new_elems:
|
for new_elem in new_elems:
|
||||||
new_obj = tool.Ifc.get_object(new_elem)
|
new_obj = tool.Ifc.get_object(new_elem)
|
||||||
@@ -1256,7 +1256,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
relating_obj=parent_obj,
|
relating_obj=parent_obj,
|
||||||
related_obj=new_obj,
|
related_obj=new_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Select all duplicated objects and their parts
|
# Select all duplicated objects and their parts
|
||||||
all_objects_to_select = set()
|
all_objects_to_select = set()
|
||||||
for old_elem, new_elems in old_to_new.items():
|
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)
|
new_obj = tool.Ifc.get_object(new_elem)
|
||||||
if new_obj:
|
if new_obj:
|
||||||
all_objects_to_select.add(new_obj)
|
all_objects_to_select.add(new_obj)
|
||||||
|
|
||||||
# If it's an aggregate, also select all its parts
|
# If it's an aggregate, also select all its parts
|
||||||
if new_elem.is_a("IfcElementAssembly"):
|
if new_elem.is_a("IfcElementAssembly"):
|
||||||
parts = tool.Aggregate.get_parts_recursively(new_elem)
|
parts = tool.Aggregate.get_parts_recursively(new_elem)
|
||||||
@@ -1272,17 +1272,17 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
part_obj = tool.Ifc.get_object(part)
|
part_obj = tool.Ifc.get_object(part)
|
||||||
if part_obj:
|
if part_obj:
|
||||||
all_objects_to_select.add(part_obj)
|
all_objects_to_select.add(part_obj)
|
||||||
|
|
||||||
# Deselect everything first
|
# Deselect everything first
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
|
||||||
# Select all the duplicated objects
|
# Select all the duplicated objects
|
||||||
for obj in all_objects_to_select:
|
for obj in all_objects_to_select:
|
||||||
obj.select_set(True)
|
obj.select_set(True)
|
||||||
|
|
||||||
if new_active_obj:
|
if new_active_obj:
|
||||||
context.view_layer.objects.active = new_active_obj
|
context.view_layer.objects.active = new_active_obj
|
||||||
|
|
||||||
return old_to_new
|
return old_to_new
|
||||||
|
|
||||||
|
|
||||||
@@ -1614,7 +1614,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if r.is_a("IfcRelAssignsToGroup")
|
if r.is_a("IfcRelAssignsToGroup")
|
||||||
if self.group_name in r.RelatingGroup.Name
|
if self.group_name in r.RelatingGroup.Name
|
||||||
).id()
|
).id()
|
||||||
|
|
||||||
# Initialize if not exists
|
# Initialize if not exists
|
||||||
if group not in original_data:
|
if group not in original_data:
|
||||||
original_data[group] = {}
|
original_data[group] = {}
|
||||||
@@ -1666,20 +1666,22 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
# Get the new group
|
# Get the new group
|
||||||
new_group_entity = next(
|
new_group_entity = next(
|
||||||
(r.RelatingGroup
|
(
|
||||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
r.RelatingGroup
|
||||||
if r.is_a("IfcRelAssignsToGroup")
|
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||||
if self.group_name in r.RelatingGroup.Name),
|
if r.is_a("IfcRelAssignsToGroup")
|
||||||
None
|
if self.group_name in r.RelatingGroup.Name
|
||||||
|
),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not new_group_entity:
|
if not new_group_entity:
|
||||||
return
|
return
|
||||||
|
|
||||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||||
if not pset:
|
if not pset:
|
||||||
return
|
return
|
||||||
|
|
||||||
index = pset["Index"]
|
index = pset["Index"]
|
||||||
|
|
||||||
# Find the matching old group by looking for the same aggregate name
|
# 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:
|
if index in group_data:
|
||||||
matching_group_id = group_id
|
matching_group_id = group_id
|
||||||
break
|
break
|
||||||
|
|
||||||
if matching_group_id is None:
|
if matching_group_id is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1709,7 +1711,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ifc_file.by_id(pset["id"]),
|
ifc_file.by_id(pset["id"]),
|
||||||
properties={"Aggregate_Index": int(original_data[matching_group_id][index]["Aggregate_Index"])},
|
properties={"Aggregate_Index": int(original_data[matching_group_id][index]["Aggregate_Index"])},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only assign container if element is not already aggregated under another element
|
# Only assign container if element is not already aggregated under another element
|
||||||
# Aggregated elements should not be in the spatial structure
|
# Aggregated elements should not be in the spatial structure
|
||||||
if not ifcopenshell.util.element.get_aggregate(element):
|
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)):
|
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
|
||||||
tool.Collector.assign(tool.Ifc.get_object(part))
|
tool.Collector.assign(tool.Ifc.get_object(part))
|
||||||
|
|
||||||
assignments = original_data[matching_group_id][index]["Assignment"]
|
assignments = original_data[matching_group_id][index]["Assignment"]
|
||||||
if assignments:
|
if assignments:
|
||||||
assign_to_annotations(obj, 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_pset = ifcopenshell.util.element.get_pset(base_instance, self.pset_name)
|
||||||
base_obj = tool.Ifc.get_object(base_instance)
|
base_obj = tool.Ifc.get_object(base_instance)
|
||||||
base_obj.name = base_pset["Name"] + "_" + str(base_pset["Aggregate_Index"])
|
base_obj.name = base_pset["Name"] + "_" + str(base_pset["Aggregate_Index"])
|
||||||
|
|
||||||
for element in instances_to_refresh:
|
for element in instances_to_refresh:
|
||||||
if element.GlobalId == base_instance.GlobalId:
|
if element.GlobalId == base_instance.GlobalId:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -433,29 +433,29 @@ class ObjectMaterialData:
|
|||||||
"""Load BBIM_MaterialLayer pset data for display in UI."""
|
"""Load BBIM_MaterialLayer pset data for display in UI."""
|
||||||
if not cls.element:
|
if not cls.element:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
pset_data = ifcopenshell.util.element.get_pset(cls.element, "BBIM_MaterialLayer")
|
pset_data = ifcopenshell.util.element.get_pset(cls.element, "BBIM_MaterialLayer")
|
||||||
if not pset_data or not pset_data.get("UseCustomOffset", False):
|
if not pset_data or not pset_data.get("UseCustomOffset", False):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Keep offset in SI units - format_distance will handle conversion
|
# Keep offset in SI units - format_distance will handle conversion
|
||||||
custom_offset_si = pset_data.get("CustomOffset", 0.0)
|
custom_offset_si = pset_data.get("CustomOffset", 0.0)
|
||||||
|
|
||||||
# Get the appropriate reference based on usage type
|
# Get the appropriate reference based on usage type
|
||||||
usage_type = tool.Model.get_usage_type(cls.element)
|
usage_type = tool.Model.get_usage_type(cls.element)
|
||||||
custom_reference = None
|
custom_reference = None
|
||||||
reference_label = None
|
reference_label = None
|
||||||
|
|
||||||
if usage_type == "LAYER2":
|
if usage_type == "LAYER2":
|
||||||
custom_reference = pset_data.get("CustomWallReference", "")
|
custom_reference = pset_data.get("CustomWallReference", "")
|
||||||
reference_label = "Wall Reference"
|
reference_label = "Wall Reference"
|
||||||
elif usage_type == "LAYER3":
|
elif usage_type == "LAYER3":
|
||||||
custom_reference = pset_data.get("CustomSlabReference", "")
|
custom_reference = pset_data.get("CustomSlabReference", "")
|
||||||
reference_label = "Slab Reference"
|
reference_label = "Slab Reference"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"use_custom_offset": pset_data.get("UseCustomOffset", False),
|
"use_custom_offset": pset_data.get("UseCustomOffset", False),
|
||||||
"custom_offset": custom_offset_si, # Store in SI units
|
"custom_offset": custom_offset_si, # Store in SI units
|
||||||
"custom_reference": custom_reference,
|
"custom_reference": custom_reference,
|
||||||
"reference_label": reference_label,
|
"reference_label": reference_label,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,10 +509,10 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
|
|||||||
bonsai.bim.helper.import_attributes(material[0], props.material_set_attributes)
|
bonsai.bim.helper.import_attributes(material[0], props.material_set_attributes)
|
||||||
else:
|
else:
|
||||||
bonsai.bim.helper.import_attributes(material, props.material_set_attributes)
|
bonsai.bim.helper.import_attributes(material, props.material_set_attributes)
|
||||||
|
|
||||||
# Load custom offset from BBIM_MaterialLayer pset
|
# Load custom offset from BBIM_MaterialLayer pset
|
||||||
tool.Model.load_custom_offset_from_pset(element, obj)
|
tool.Model.load_custom_offset_from_pset(element, obj)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def import_attributes_callback(
|
def import_attributes_callback(
|
||||||
@@ -625,7 +625,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
obj_material_usage.ReferenceExtent = material.ReferenceExtent
|
obj_material_usage.ReferenceExtent = material.ReferenceExtent
|
||||||
|
|
||||||
layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet)
|
layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet)
|
||||||
|
|
||||||
# Save custom offset to BBIM_MaterialLayer pset
|
# Save custom offset to BBIM_MaterialLayer pset
|
||||||
tool.Model.save_custom_offset_to_pset(obj_element, obj)
|
tool.Model.save_custom_offset_to_pset(obj_element, obj)
|
||||||
|
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ class BIM_PT_object_material(Panel):
|
|||||||
# Material Set Attributes Section
|
# Material Set Attributes Section
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
box = row.box()
|
box = row.box()
|
||||||
|
|
||||||
bonsai.bim.helper.draw_attributes(self.props.material_set_attributes, box)
|
bonsai.bim.helper.draw_attributes(self.props.material_set_attributes, box)
|
||||||
bonsai.bim.helper.draw_attributes(self.props.material_set_usage_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",
|
"layer": "Material Layers",
|
||||||
"profile": "Material Profiles",
|
"profile": "Material Profiles",
|
||||||
"constituent": "Material Constituents",
|
"constituent": "Material Constituents",
|
||||||
"list_item": "Material List Items"
|
"list_item": "Material List Items",
|
||||||
}
|
}
|
||||||
header_text = header_map.get(set_item_name, "Material Items")
|
header_text = header_map.get(set_item_name, "Material Items")
|
||||||
self.layout.label(text=header_text)
|
self.layout.label(text=header_text)
|
||||||
@@ -255,7 +255,7 @@ class BIM_PT_object_material(Panel):
|
|||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
box = row.box()
|
box = row.box()
|
||||||
|
|
||||||
# Add Material Section (at the top of this box)
|
# Add Material Section (at the top of this box)
|
||||||
if ObjectMaterialData.data["set_item_name"] == "profile" and not self.mprops.profiles:
|
if ObjectMaterialData.data["set_item_name"] == "profile" and not self.mprops.profiles:
|
||||||
box_row = box.row(align=True)
|
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="")
|
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="")
|
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"])
|
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
|
||||||
|
|
||||||
active_object = bpy.context.active_object
|
active_object = bpy.context.active_object
|
||||||
self.layerset_bounds(box, active_object, location="Top_Interior")
|
self.layerset_bounds(box, active_object, location="Top_Interior")
|
||||||
|
|
||||||
@@ -356,7 +356,7 @@ class BIM_PT_object_material(Panel):
|
|||||||
# Material Set Information Section
|
# Material Set Information Section
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
box = row.box()
|
box = row.box()
|
||||||
|
|
||||||
if ObjectMaterialData.data["material_class"] != "IfcMaterialList":
|
if ObjectMaterialData.data["material_class"] != "IfcMaterialList":
|
||||||
box_row = box.row(align=True)
|
box_row = box.row(align=True)
|
||||||
set_name = ObjectMaterialData.data["set"]["name"]
|
set_name = ObjectMaterialData.data["set"]["name"]
|
||||||
@@ -395,6 +395,7 @@ class BIM_PT_object_material(Panel):
|
|||||||
if unit_system == "IMPERIAL":
|
if unit_system == "IMPERIAL":
|
||||||
precision = prefs.doc.imperial_precision
|
precision = prefs.doc.imperial_precision
|
||||||
from bonsai.bim.module.drawing.helper import format_distance
|
from bonsai.bim.module.drawing.helper import format_distance
|
||||||
|
|
||||||
formatted_offset = format_distance(
|
formatted_offset = format_distance(
|
||||||
offset_value, precision=precision, suppress_zero_inches=True, in_unit_length=True
|
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
|
# BBIM_MaterialLayer Pset Section
|
||||||
if pset_data := ObjectMaterialData.data.get("bbim_material_layer_pset"):
|
if pset_data := ObjectMaterialData.data.get("bbim_material_layer_pset"):
|
||||||
self.layout.label(text="BBIM_MaterialLayer Pset")
|
self.layout.label(text="BBIM_MaterialLayer Pset")
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
box = row.box()
|
box = row.box()
|
||||||
|
|
||||||
# Custom Offset value - format using format_distance
|
# Custom Offset value - format using format_distance
|
||||||
unit_system = bpy.context.scene.unit_settings.system
|
unit_system = bpy.context.scene.unit_settings.system
|
||||||
prefs = tool.Blender.get_addon_preferences()
|
prefs = tool.Blender.get_addon_preferences()
|
||||||
@@ -416,13 +417,14 @@ class BIM_PT_object_material(Panel):
|
|||||||
if unit_system == "IMPERIAL":
|
if unit_system == "IMPERIAL":
|
||||||
precision = prefs.doc.imperial_precision
|
precision = prefs.doc.imperial_precision
|
||||||
from bonsai.bim.module.drawing.helper import format_distance
|
from bonsai.bim.module.drawing.helper import format_distance
|
||||||
|
|
||||||
formatted_custom_offset = 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 = box.row(align=True)
|
||||||
box_row.label(text="Custom Offset")
|
box_row.label(text="Custom Offset")
|
||||||
box_row.label(text=formatted_custom_offset)
|
box_row.label(text=formatted_custom_offset)
|
||||||
|
|
||||||
# Reference (if exists)
|
# Reference (if exists)
|
||||||
if pset_data["custom_reference"]:
|
if pset_data["custom_reference"]:
|
||||||
box_row = box.row(align=True)
|
box_row = box.row(align=True)
|
||||||
@@ -436,12 +438,12 @@ class BIM_PT_object_material(Panel):
|
|||||||
"layer": "Material Layers",
|
"layer": "Material Layers",
|
||||||
"profile": "Material Profiles",
|
"profile": "Material Profiles",
|
||||||
"constituent": "Material Constituents",
|
"constituent": "Material Constituents",
|
||||||
"list_item": "Material List Items"
|
"list_item": "Material List Items",
|
||||||
}
|
}
|
||||||
header_text = header_map.get(set_item_name, "Material Items")
|
header_text = header_map.get(set_item_name, "Material Items")
|
||||||
else:
|
else:
|
||||||
header_text = "Materials"
|
header_text = "Materials"
|
||||||
|
|
||||||
self.layout.label(text=header_text)
|
self.layout.label(text=header_text)
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
box = row.box()
|
box = row.box()
|
||||||
@@ -492,11 +494,11 @@ class BIM_PT_object_material(Panel):
|
|||||||
if layer_set_direction:
|
if layer_set_direction:
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.label(text="BBIM_MaterialLayer Pset")
|
row.label(text="BBIM_MaterialLayer Pset")
|
||||||
|
|
||||||
# Add indentation with a row that has a separator
|
# Add indentation with a row that has a separator
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
# row.separator(factor=2.0) # Adjust factor for more/less indent
|
# row.separator(factor=2.0) # Adjust factor for more/less indent
|
||||||
|
|
||||||
box = row.box()
|
box = row.box()
|
||||||
box_row = box.row(align=True)
|
box_row = box.row(align=True)
|
||||||
box_row.prop(self.props, "use_custom_offset", text="Use Custom Offset")
|
box_row.prop(self.props, "use_custom_offset", text="Use Custom Offset")
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ class FilledOpeningGenerator:
|
|||||||
reuse_mapped_representation = True
|
reuse_mapped_representation = True
|
||||||
else:
|
else:
|
||||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||||
|
|
||||||
if not reuse_mapped_representation:
|
if not reuse_mapped_representation:
|
||||||
# Check for library template before generating from filling
|
# Check for library template before generating from filling
|
||||||
template_rep = self.get_opening_template_from_type(filling)
|
template_rep = self.get_opening_template_from_type(filling)
|
||||||
@@ -191,25 +191,25 @@ class FilledOpeningGenerator:
|
|||||||
MappingSource=existing_mapping_source,
|
MappingSource=existing_mapping_source,
|
||||||
MappingTarget=tool.Ifc.get().create_entity(
|
MappingTarget=tool.Ifc.get().create_entity(
|
||||||
"IfcCartesianTransformationOperator3D",
|
"IfcCartesianTransformationOperator3D",
|
||||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1., 0., 0.)),
|
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
|
||||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 1., 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.)),
|
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
|
||||||
Scale=1.,
|
Scale=1.0,
|
||||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 0., 1.))
|
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
mapped_representation = tool.Ifc.get().create_entity(
|
mapped_representation = tool.Ifc.get().create_entity(
|
||||||
"IfcShapeRepresentation",
|
"IfcShapeRepresentation",
|
||||||
ContextOfItems=context,
|
ContextOfItems=context,
|
||||||
RepresentationIdentifier="Body",
|
RepresentationIdentifier="Body",
|
||||||
RepresentationType="MappedRepresentation",
|
RepresentationType="MappedRepresentation",
|
||||||
Items=[new_mapped_item]
|
Items=[new_mapped_item],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||||
tool.Ifc.get(), representation=representation
|
tool.Ifc.get(), representation=representation
|
||||||
)
|
)
|
||||||
|
|
||||||
ifcopenshell.api.geometry.assign_representation(
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||||
)
|
)
|
||||||
@@ -333,25 +333,25 @@ class FilledOpeningGenerator:
|
|||||||
MappingSource=existing_mapping_source,
|
MappingSource=existing_mapping_source,
|
||||||
MappingTarget=tool.Ifc.get().create_entity(
|
MappingTarget=tool.Ifc.get().create_entity(
|
||||||
"IfcCartesianTransformationOperator3D",
|
"IfcCartesianTransformationOperator3D",
|
||||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1., 0., 0.)),
|
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
|
||||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 1., 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.)),
|
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
|
||||||
Scale=1.,
|
Scale=1.0,
|
||||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0., 0., 1.))
|
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
mapped_representation = tool.Ifc.get().create_entity(
|
mapped_representation = tool.Ifc.get().create_entity(
|
||||||
"IfcShapeRepresentation",
|
"IfcShapeRepresentation",
|
||||||
ContextOfItems=context,
|
ContextOfItems=context,
|
||||||
RepresentationIdentifier="Body",
|
RepresentationIdentifier="Body",
|
||||||
RepresentationType="MappedRepresentation",
|
RepresentationType="MappedRepresentation",
|
||||||
Items=[new_mapped_item]
|
Items=[new_mapped_item],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||||
tool.Ifc.get(), representation=representation_to_use
|
tool.Ifc.get(), representation=representation_to_use
|
||||||
)
|
)
|
||||||
|
|
||||||
ifcopenshell.api.geometry.assign_representation(
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -297,13 +297,13 @@ class DumbSlabPlaner:
|
|||||||
extrusion = tool.Model.get_extrusion(representation)
|
extrusion = tool.Model.get_extrusion(representation)
|
||||||
if extrusion:
|
if extrusion:
|
||||||
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||||
|
|
||||||
# Calculate the actual extrusion angle from vertical
|
# Calculate the actual extrusion angle from vertical
|
||||||
extrusion_angle = 0
|
extrusion_angle = 0
|
||||||
if direction_ratios.length > 0:
|
if direction_ratios.length > 0:
|
||||||
cos_angle = direction_ratios.normalized().dot(Vector((0, 0, 1)))
|
cos_angle = direction_ratios.normalized().dot(Vector((0, 0, 1)))
|
||||||
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
||||||
|
|
||||||
# FIX: Only apply 1/cos factor when there's actual extrusion slope
|
# FIX: Only apply 1/cos factor when there's actual extrusion slope
|
||||||
if extrusion_angle > 1e-6:
|
if extrusion_angle > 1e-6:
|
||||||
perpendicular_depth = thickness * abs(1 / cos(extrusion_angle))
|
perpendicular_depth = thickness * abs(1 / cos(extrusion_angle))
|
||||||
@@ -311,13 +311,13 @@ class DumbSlabPlaner:
|
|||||||
else:
|
else:
|
||||||
perpendicular_depth = thickness
|
perpendicular_depth = thickness
|
||||||
perpendicular_offset = layer_offset / self.unit_scale
|
perpendicular_offset = layer_offset / self.unit_scale
|
||||||
|
|
||||||
# Check if direction sense needs to be applied
|
# Check if direction sense needs to be applied
|
||||||
# This should only happen if explicitly requested, not automatically
|
# This should only happen if explicitly requested, not automatically
|
||||||
if layer_params.get("apply_direction_sense", False):
|
if layer_params.get("apply_direction_sense", False):
|
||||||
# Store current direction before potential change
|
# Store current direction before potential change
|
||||||
old_direction = direction_ratios.copy()
|
old_direction = direction_ratios.copy()
|
||||||
|
|
||||||
# Apply direction sense logic
|
# Apply direction sense logic
|
||||||
existing_x_angle = extrusion_angle
|
existing_x_angle = extrusion_angle
|
||||||
if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or (
|
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
|
offset_direction = direction_ratios.copy() * -1
|
||||||
if layer_params["direction_sense"] == "POSITIVE":
|
if layer_params["direction_sense"] == "POSITIVE":
|
||||||
direction_ratios *= -1
|
direction_ratios *= -1
|
||||||
|
|
||||||
# If direction changed, update extrusion with rotation compensation
|
# If direction changed, update extrusion with rotation compensation
|
||||||
if (direction_ratios.normalized() - old_direction.normalized()).length > 1e-6:
|
if (direction_ratios.normalized() - old_direction.normalized()).length > 1e-6:
|
||||||
update_extrusion_direction(element, tuple(direction_ratios), obj)
|
update_extrusion_direction(element, tuple(direction_ratios), obj)
|
||||||
# After updating direction, get the updated extrusion
|
# After updating direction, get the updated extrusion
|
||||||
extrusion = tool.Model.get_extrusion(representation)
|
extrusion = tool.Model.get_extrusion(representation)
|
||||||
|
|
||||||
# Update depth
|
# Update depth
|
||||||
extrusion.Depth = perpendicular_depth
|
extrusion.Depth = perpendicular_depth
|
||||||
|
|
||||||
# Update position
|
# Update position
|
||||||
ifc_position = extrusion.Position
|
ifc_position = extrusion.Position
|
||||||
if direction_ratios.length > 0:
|
if direction_ratios.length > 0:
|
||||||
offset_vector = direction_ratios.normalized() * perpendicular_offset
|
offset_vector = direction_ratios.normalized() * perpendicular_offset
|
||||||
position = offset_vector
|
position = offset_vector
|
||||||
|
|
||||||
material = ifcopenshell.util.element.get_material(element)
|
material = ifcopenshell.util.element.get_material(element)
|
||||||
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
if material and material.is_a("IfcMaterialLayerSetUsage"):
|
||||||
material.OffsetFromReferenceLine = position.z
|
material.OffsetFromReferenceLine = position.z
|
||||||
|
|
||||||
if ifc_position:
|
if ifc_position:
|
||||||
ifc_position.Location.Coordinates = position
|
ifc_position.Location.Coordinates = position
|
||||||
else:
|
else:
|
||||||
@@ -397,13 +397,12 @@ class DumbSlabPlaner:
|
|||||||
representation=representation,
|
representation=representation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_extrusion_direction(
|
||||||
def update_extrusion_direction(element: ifcopenshell.entity_instance,
|
element: ifcopenshell.entity_instance, new_direction_ratios: tuple, obj: bpy.types.Object = None
|
||||||
new_direction_ratios: tuple,
|
) -> None:
|
||||||
obj: bpy.types.Object = None) -> None:
|
|
||||||
"""
|
"""
|
||||||
Update extrusion direction while preserving overall object orientation.
|
Update extrusion direction while preserving overall object orientation.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
element: The IFC element
|
element: The IFC element
|
||||||
new_direction_ratios: New extrusion direction ratios (x,y,z)
|
new_direction_ratios: New extrusion direction ratios (x,y,z)
|
||||||
@@ -413,66 +412,66 @@ class DumbSlabPlaner:
|
|||||||
obj = tool.Ifc.get_object(element)
|
obj = tool.Ifc.get_object(element)
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
|
|
||||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||||
if not representation:
|
if not representation:
|
||||||
return
|
return
|
||||||
|
|
||||||
extrusion = tool.Model.get_extrusion(representation)
|
extrusion = tool.Model.get_extrusion(representation)
|
||||||
if not extrusion:
|
if not extrusion:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get current extrusion direction
|
# Get current extrusion direction
|
||||||
old_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
old_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||||
if old_direction.length == 0:
|
if old_direction.length == 0:
|
||||||
old_direction = Vector((0, 0, 1)) # Default
|
old_direction = Vector((0, 0, 1)) # Default
|
||||||
|
|
||||||
new_direction = Vector(new_direction_ratios)
|
new_direction = Vector(new_direction_ratios)
|
||||||
if new_direction.length == 0:
|
if new_direction.length == 0:
|
||||||
new_direction = Vector((0, 0, 1)) # Default
|
new_direction = Vector((0, 0, 1)) # Default
|
||||||
|
|
||||||
# Normalize both directions
|
# Normalize both directions
|
||||||
old_direction_normalized = old_direction.normalized()
|
old_direction_normalized = old_direction.normalized()
|
||||||
new_direction_normalized = new_direction.normalized()
|
new_direction_normalized = new_direction.normalized()
|
||||||
|
|
||||||
# Store current object matrix
|
# Store current object matrix
|
||||||
old_matrix = obj.matrix_world.copy()
|
old_matrix = obj.matrix_world.copy()
|
||||||
|
|
||||||
# Calculate the rotation needed to keep same orientation
|
# Calculate the rotation needed to keep same orientation
|
||||||
# When extrusion direction changes from A to B relative to local coordinates,
|
# When extrusion direction changes from A to B relative to local coordinates,
|
||||||
# we need to rotate the object by the inverse of that change
|
# we need to rotate the object by the inverse of that change
|
||||||
|
|
||||||
# Calculate rotation from old to new direction
|
# Calculate rotation from old to new direction
|
||||||
rotation_axis = old_direction_normalized.cross(new_direction_normalized)
|
rotation_axis = old_direction_normalized.cross(new_direction_normalized)
|
||||||
if rotation_axis.length > 1e-6:
|
if rotation_axis.length > 1e-6:
|
||||||
rotation_axis.normalized()
|
rotation_axis.normalized()
|
||||||
dot_product = old_direction_normalized.dot(new_direction_normalized)
|
dot_product = old_direction_normalized.dot(new_direction_normalized)
|
||||||
angle = acos(min(max(dot_product, -1), 1))
|
angle = acos(min(max(dot_product, -1), 1))
|
||||||
|
|
||||||
# Apply INVERSE rotation to object to compensate
|
# Apply INVERSE rotation to object to compensate
|
||||||
rotation_matrix = Matrix.Rotation(-angle, 4, rotation_axis)
|
rotation_matrix = Matrix.Rotation(-angle, 4, rotation_axis)
|
||||||
|
|
||||||
# Update object rotation
|
# Update object rotation
|
||||||
obj.matrix_world = old_matrix @ rotation_matrix
|
obj.matrix_world = old_matrix @ rotation_matrix
|
||||||
bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
# Update extrusion direction (keeping magnitude)
|
# Update extrusion direction (keeping magnitude)
|
||||||
if old_direction.length > 0:
|
if old_direction.length > 0:
|
||||||
# Preserve the magnitude of the original direction vector
|
# Preserve the magnitude of the original direction vector
|
||||||
magnitude = old_direction.length
|
magnitude = old_direction.length
|
||||||
new_direction = new_direction_normalized * magnitude
|
new_direction = new_direction_normalized * magnitude
|
||||||
|
|
||||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_direction)
|
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_direction)
|
||||||
|
|
||||||
# Update depth based on new extrusion angle
|
# Update depth based on new extrusion angle
|
||||||
extrusion_angle = 0
|
extrusion_angle = 0
|
||||||
if new_direction.length > 0:
|
if new_direction.length > 0:
|
||||||
cos_angle = new_direction_normalized.dot(Vector((0, 0, 1)))
|
cos_angle = new_direction_normalized.dot(Vector((0, 0, 1)))
|
||||||
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
extrusion_angle = acos(min(max(cos_angle, -1), 1))
|
||||||
|
|
||||||
# Get current depth (perpendicular depth)
|
# Get current depth (perpendicular depth)
|
||||||
current_perpendicular_depth = extrusion.Depth
|
current_perpendicular_depth = extrusion.Depth
|
||||||
|
|
||||||
# If we have material layer info, calculate actual thickness
|
# If we have material layer info, calculate actual thickness
|
||||||
material = ifcopenshell.util.element.get_material(element)
|
material = ifcopenshell.util.element.get_material(element)
|
||||||
actual_thickness = current_perpendicular_depth
|
actual_thickness = current_perpendicular_depth
|
||||||
@@ -481,15 +480,15 @@ class DumbSlabPlaner:
|
|||||||
actual_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
actual_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
actual_thickness *= unit_scale
|
actual_thickness *= unit_scale
|
||||||
|
|
||||||
# Convert to perpendicular depth if needed
|
# Convert to perpendicular depth if needed
|
||||||
if extrusion_angle > 1e-6:
|
if extrusion_angle > 1e-6:
|
||||||
new_perpendicular_depth = actual_thickness * abs(1 / cos(extrusion_angle))
|
new_perpendicular_depth = actual_thickness * abs(1 / cos(extrusion_angle))
|
||||||
else:
|
else:
|
||||||
new_perpendicular_depth = actual_thickness
|
new_perpendicular_depth = actual_thickness
|
||||||
|
|
||||||
extrusion.Depth = new_perpendicular_depth
|
extrusion.Depth = new_perpendicular_depth
|
||||||
|
|
||||||
# Update position offset if needed
|
# Update position offset if needed
|
||||||
if extrusion.Position:
|
if extrusion.Position:
|
||||||
# Recalculate offset based on new direction
|
# Recalculate offset based on new direction
|
||||||
@@ -500,7 +499,7 @@ class DumbSlabPlaner:
|
|||||||
perpendicular_offset = offset * abs(1 / cos(extrusion_angle))
|
perpendicular_offset = offset * abs(1 / cos(extrusion_angle))
|
||||||
else:
|
else:
|
||||||
perpendicular_offset = offset
|
perpendicular_offset = offset
|
||||||
|
|
||||||
offset_vector = new_direction_normalized * perpendicular_offset
|
offset_vector = new_direction_normalized * perpendicular_offset
|
||||||
extrusion.Position.Location.Coordinates = tuple(offset_vector)
|
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)
|
extrusion = tool.Model.get_extrusion(body)
|
||||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||||
|
|
||||||
usage_type = tool.Model.get_usage_type(element)
|
usage_type = tool.Model.get_usage_type(element)
|
||||||
|
|
||||||
if extrusion.Position:
|
if extrusion.Position:
|
||||||
@@ -798,7 +797,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
# Store original rotation for later restoration
|
# Store original rotation for later restoration
|
||||||
original_rotation_x = obj.rotation_euler.x
|
original_rotation_x = obj.rotation_euler.x
|
||||||
obj["pre_edit_rotation_x"] = original_rotation_x
|
obj["pre_edit_rotation_x"] = original_rotation_x
|
||||||
|
|
||||||
# Reset rotation to zero - profile will be horizontal
|
# Reset rotation to zero - profile will be horizontal
|
||||||
current_z_rot = obj.rotation_euler.z
|
current_z_rot = obj.rotation_euler.z
|
||||||
obj.rotation_euler.x = 0.0
|
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
|
# 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
|
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
|
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||||
|
|
||||||
# Import with x_angle=0
|
# Import with x_angle=0
|
||||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position, 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
|
# 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:
|
for vert in obj.data.vertices:
|
||||||
vert.co.y *= scale_factor
|
vert.co.y *= scale_factor
|
||||||
else:
|
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))
|
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context))
|
||||||
if not bpy.app.background:
|
if not bpy.app.background:
|
||||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -858,7 +857,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||||
usage_type = tool.Model.get_usage_type(element)
|
usage_type = tool.Model.get_usage_type(element)
|
||||||
|
|
||||||
if extrusion.Position:
|
if extrusion.Position:
|
||||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||||
position.translation *= self.unit_scale
|
position.translation *= self.unit_scale
|
||||||
@@ -895,16 +894,17 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
# Scale Y coordinates back up before exporting
|
# Scale Y coordinates back up before exporting
|
||||||
obj_x_rotation = obj.rotation_euler.x
|
obj_x_rotation = obj.rotation_euler.x
|
||||||
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
scale_factor = abs(cos(obj_x_rotation)) if abs(obj_x_rotation) > 1e-6 else 1.0
|
||||||
|
|
||||||
# Un-scale the profile before exporting
|
# Un-scale the profile before exporting
|
||||||
for vert in obj.data.vertices:
|
for vert in obj.data.vertices:
|
||||||
vert.co.y /= scale_factor # Inverse of import scaling
|
vert.co.y /= scale_factor # Inverse of import scaling
|
||||||
|
|
||||||
profile = tool.Model.export_profile(obj, position=position, x_angle=0)
|
profile = tool.Model.export_profile(obj, position=position, x_angle=0)
|
||||||
else:
|
else:
|
||||||
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
profile = tool.Model.export_profile(obj, position=position, x_angle=existing_x_angle)
|
||||||
|
|
||||||
if not profile:
|
if not profile:
|
||||||
|
|
||||||
def msg(self, context):
|
def msg(self, context):
|
||||||
self.layout.label(text="INVALID PROFILE")
|
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
|
tool.Ifc.get(), product=element, representation=new_footprint
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
footprint_context = ifcopenshell.util.representation.get_context(
|
footprint_context = ifcopenshell.util.representation.get_context(
|
||||||
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
|
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -397,28 +397,28 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
for obj in selected_objs:
|
for obj in selected_objs:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
assert element
|
assert element
|
||||||
|
|
||||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||||
if not representation:
|
if not representation:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
extrusion = tool.Model.get_extrusion(representation)
|
extrusion = tool.Model.get_extrusion(representation)
|
||||||
if not extrusion:
|
if not extrusion:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get extrusion direction
|
# Get extrusion direction
|
||||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||||
|
|
||||||
# Calculate angle from vertical
|
# Calculate angle from vertical
|
||||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||||
|
|
||||||
# For sloped walls, compensate so VERTICAL height = target depth
|
# For sloped walls, compensate so VERTICAL height = target depth
|
||||||
cos_angle = cos(x_angle)
|
cos_angle = cos(x_angle)
|
||||||
compensation_factor = abs(1 / cos_angle) if abs(cos_angle) > 1e-6 else 1.0
|
compensation_factor = abs(1 / cos_angle) if abs(cos_angle) > 1e-6 else 1.0
|
||||||
new_depth_ifc = (self.depth / si_conversion) * compensation_factor
|
new_depth_ifc = (self.depth / si_conversion) * compensation_factor
|
||||||
|
|
||||||
extrusion.Depth = new_depth_ifc
|
extrusion.Depth = new_depth_ifc
|
||||||
|
|
||||||
# IMPORTANT: Refresh the geometry to reflect the IFC changes
|
# IMPORTANT: Refresh the geometry to reflect the IFC changes
|
||||||
bonsai.core.geometry.switch_representation(
|
bonsai.core.geometry.switch_representation(
|
||||||
tool.Ifc,
|
tool.Ifc,
|
||||||
@@ -426,7 +426,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
obj=obj,
|
obj=obj,
|
||||||
representation=representation,
|
representation=representation,
|
||||||
)
|
)
|
||||||
|
|
||||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||||
for rel in element.ConnectedFrom:
|
for rel in element.ConnectedFrom:
|
||||||
if rel.is_a() == "IfcRelConnectsElements":
|
if rel.is_a() == "IfcRelConnectsElements":
|
||||||
@@ -436,7 +436,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
if layer2_objs:
|
if layer2_objs:
|
||||||
tool.Model.recalculate_walls(layer2_objs)
|
tool.Model.recalculate_walls(layer2_objs)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -471,51 +471,55 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
extrusion = tool.Model.get_extrusion(representation)
|
extrusion = tool.Model.get_extrusion(representation)
|
||||||
if not extrusion:
|
if not extrusion:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get current object rotation matrix
|
# Get current object rotation matrix
|
||||||
obj_rotation = obj.matrix_world.to_3x3()
|
obj_rotation = obj.matrix_world.to_3x3()
|
||||||
|
|
||||||
# Get current extrusion direction in LOCAL coordinates
|
# Get current extrusion direction in LOCAL coordinates
|
||||||
current_local_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
current_local_direction = Vector(extrusion.ExtrudedDirection.DirectionRatios)
|
||||||
if current_local_direction.length == 0:
|
if current_local_direction.length == 0:
|
||||||
current_local_direction = Vector((0, 0, 1))
|
current_local_direction = Vector((0, 0, 1))
|
||||||
current_local_direction_normalized = current_local_direction.normalized()
|
current_local_direction_normalized = current_local_direction.normalized()
|
||||||
|
|
||||||
# Calculate what the current extrusion direction is in WORLD coordinates
|
# Calculate what the current extrusion direction is in WORLD coordinates
|
||||||
current_world_direction = obj_rotation @ current_local_direction_normalized
|
current_world_direction = obj_rotation @ current_local_direction_normalized
|
||||||
|
|
||||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
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, 0, tolerance=0.001) else existing_x_angle
|
||||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||||
|
|
||||||
# Calculate the NEW local extrusion direction based on x_angle
|
# Calculate the NEW local extrusion direction based on x_angle
|
||||||
new_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
|
new_local_direction = Vector((0.0, sin(x_angle), cos(x_angle)))
|
||||||
|
|
||||||
# Check if extrusion direction is actually changing
|
# Check if extrusion direction is actually changing
|
||||||
current_local_norm = current_local_direction_normalized
|
current_local_norm = current_local_direction_normalized
|
||||||
new_local_norm = new_local_direction.normalized()
|
new_local_norm = new_local_direction.normalized()
|
||||||
|
|
||||||
# Compare the LOCAL directions
|
# Compare the LOCAL directions
|
||||||
local_direction_changed = (new_local_norm - current_local_norm).length > 1e-6
|
local_direction_changed = (new_local_norm - current_local_norm).length > 1e-6
|
||||||
|
|
||||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||||
depth = extrusion.Depth / abs(1 / cos(existing_x_angle))
|
depth = extrusion.Depth / abs(1 / cos(existing_x_angle))
|
||||||
perpendicular_depth = depth * abs(1 / cos(x_angle))
|
perpendicular_depth = depth * abs(1 / cos(x_angle))
|
||||||
|
|
||||||
# Update extrusion direction
|
# Update extrusion direction
|
||||||
if local_direction_changed:
|
if local_direction_changed:
|
||||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_local_direction)
|
extrusion.ExtrudedDirection.DirectionRatios = tuple(new_local_direction)
|
||||||
|
|
||||||
# Always update depth
|
# Always update depth
|
||||||
extrusion.Depth = perpendicular_depth
|
extrusion.Depth = perpendicular_depth
|
||||||
layer2_objs.append(obj)
|
layer2_objs.append(obj)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if tool.Model.get_usage_type(element) == "LAYER3":
|
if tool.Model.get_usage_type(element) == "LAYER3":
|
||||||
# For slabs, handle polyline scaling
|
# For slabs, handle polyline scaling
|
||||||
existing_obj_x_angle = obj.rotation_euler.x
|
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 = (
|
||||||
existing_obj_x_angle = 0 if tool.Cad.is_x(existing_obj_x_angle, pi, tolerance=0.001) else 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
|
# Scale the polyline coordinates
|
||||||
coord_list = builder.get_polyline_coords(extrusion.SweptArea.OuterCurve)
|
coord_list = builder.get_polyline_coords(extrusion.SweptArea.OuterCurve)
|
||||||
@@ -547,11 +551,11 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
offset_direction *= -1
|
offset_direction *= -1
|
||||||
if layer_params["direction_sense"] == "POSITIVE":
|
if layer_params["direction_sense"] == "POSITIVE":
|
||||||
final_local_direction *= -1
|
final_local_direction *= -1
|
||||||
|
|
||||||
# Check if extrusion direction actually changed
|
# Check if extrusion direction actually changed
|
||||||
final_local_norm = final_local_direction.normalized()
|
final_local_norm = final_local_direction.normalized()
|
||||||
local_direction_changed = (final_local_norm - current_local_norm).length > 1e-6
|
local_direction_changed = (final_local_norm - current_local_norm).length > 1e-6
|
||||||
|
|
||||||
# Update extrusion properties
|
# Update extrusion properties
|
||||||
extrusion.ExtrudedDirection.DirectionRatios = tuple(final_local_direction)
|
extrusion.ExtrudedDirection.DirectionRatios = tuple(final_local_direction)
|
||||||
extrusion.Depth = perpendicular_depth
|
extrusion.Depth = perpendicular_depth
|
||||||
@@ -559,19 +563,19 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if extrusion.Position or perpendicular_offset != 0:
|
if extrusion.Position or perpendicular_offset != 0:
|
||||||
position = offset_direction * perpendicular_offset
|
position = offset_direction * perpendicular_offset
|
||||||
tool.Model.add_extrusion_position(extrusion, position)
|
tool.Model.add_extrusion_position(extrusion, position)
|
||||||
|
|
||||||
# Adjust object rotation if extrusion direction changed
|
# Adjust object rotation if extrusion direction changed
|
||||||
if local_direction_changed:
|
if local_direction_changed:
|
||||||
# Calculate what the NEW world direction would be with current object rotation
|
# Calculate what the NEW world direction would be with current object rotation
|
||||||
expected_new_world_direction = obj_rotation @ final_local_norm
|
expected_new_world_direction = obj_rotation @ final_local_norm
|
||||||
|
|
||||||
# The rotation needed is from expected_new_world_direction to current_world_direction
|
# The rotation needed is from expected_new_world_direction to current_world_direction
|
||||||
rotation_axis = expected_new_world_direction.cross(current_world_direction)
|
rotation_axis = expected_new_world_direction.cross(current_world_direction)
|
||||||
if rotation_axis.length > 1e-6:
|
if rotation_axis.length > 1e-6:
|
||||||
rotation_axis.normalize()
|
rotation_axis.normalize()
|
||||||
dot_product = expected_new_world_direction.dot(current_world_direction)
|
dot_product = expected_new_world_direction.dot(current_world_direction)
|
||||||
angle = acos(min(max(dot_product, -1), 1))
|
angle = acos(min(max(dot_product, -1), 1))
|
||||||
|
|
||||||
# Create and apply rotation matrix
|
# Create and apply rotation matrix
|
||||||
rotation_matrix = Matrix.Rotation(angle, 4, rotation_axis)
|
rotation_matrix = Matrix.Rotation(angle, 4, rotation_axis)
|
||||||
obj.matrix_world = rotation_matrix @ obj.matrix_world
|
obj.matrix_world = rotation_matrix @ obj.matrix_world
|
||||||
@@ -1081,7 +1085,7 @@ class DumbWallGenerator:
|
|||||||
obj=obj,
|
obj=obj,
|
||||||
representation=representation,
|
representation=representation,
|
||||||
)
|
)
|
||||||
|
|
||||||
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
|
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"})
|
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
|
||||||
material = ifcopenshell.util.element.get_material(element)
|
material = ifcopenshell.util.element.get_material(element)
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ class EditItemUI:
|
|||||||
assert obj
|
assert obj
|
||||||
|
|
||||||
mesh_props = tool.Geometry.get_mesh_props(obj.data)
|
mesh_props = tool.Geometry.get_mesh_props(obj.data)
|
||||||
|
|
||||||
# Get the parent element from representation_obj to check for layer set usage
|
# Get the parent element from representation_obj to check for layer set usage
|
||||||
has_layer_set_usage = False
|
has_layer_set_usage = False
|
||||||
props = tool.Geometry.get_geometry_props()
|
props = tool.Geometry.get_geometry_props()
|
||||||
@@ -408,7 +408,7 @@ class EditItemUI:
|
|||||||
if parent_element:
|
if parent_element:
|
||||||
material_usage = tool.Model.get_usage_type(parent_element)
|
material_usage = tool.Model.get_usage_type(parent_element)
|
||||||
has_layer_set_usage = material_usage == "LAYER3"
|
has_layer_set_usage = material_usage == "LAYER3"
|
||||||
|
|
||||||
if AuthoringData.data["is_representation_item_swept_solid"]:
|
if AuthoringData.data["is_representation_item_swept_solid"]:
|
||||||
# TODO: support EndSweptArea for IfcRevolvedAreaSolidTapered,
|
# TODO: support EndSweptArea for IfcRevolvedAreaSolidTapered,
|
||||||
# will need to add second attribute for this.
|
# will need to add second attribute for this.
|
||||||
@@ -427,7 +427,7 @@ class EditItemUI:
|
|||||||
continue
|
continue
|
||||||
row = cls.layout.row()
|
row = cls.layout.row()
|
||||||
draw_attribute(item_attribute, cls.layout)
|
draw_attribute(item_attribute, cls.layout)
|
||||||
|
|
||||||
if len(mesh_props.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]:
|
if len(mesh_props.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]:
|
||||||
row = cls.layout.row()
|
row = cls.layout.row()
|
||||||
row.operator("bim.update_item_attributes", icon="FILE_REFRESH", text="")
|
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
|
and not self.is_advanced
|
||||||
):
|
):
|
||||||
filepath = self.get_filepath()
|
filepath = self.get_filepath()
|
||||||
|
|
||||||
# First, load the IFC file temporarily to check for metadata document
|
# First, load the IFC file temporarily to check for metadata document
|
||||||
temp_ifc = None
|
temp_ifc = None
|
||||||
has_metadata_doc = False
|
has_metadata_doc = False
|
||||||
@@ -1076,7 +1076,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
temp_ifc = None
|
temp_ifc = None
|
||||||
|
|
||||||
if has_metadata_doc:
|
if has_metadata_doc:
|
||||||
suffix = tool.Blender.get_addon_preferences().metadata_blend_file_suffix
|
suffix = tool.Blender.get_addon_preferences().metadata_blend_file_suffix
|
||||||
if str(filepath).lower().endswith(".ifc"):
|
if str(filepath).lower().endswith(".ifc"):
|
||||||
@@ -1137,10 +1137,10 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
props.is_loading = True
|
props.is_loading = True
|
||||||
props.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
props.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
||||||
props.use_relative_project_path = self.use_relative_path
|
props.use_relative_project_path = self.use_relative_path
|
||||||
|
|
||||||
metadata_doc = tool.Project.get_metadata_document_information()
|
metadata_doc = tool.Project.get_metadata_document_information()
|
||||||
props.should_save_metadata_for_this_file = metadata_doc is not None
|
props.should_save_metadata_for_this_file = metadata_doc is not None
|
||||||
|
|
||||||
tool.Blender.register_toolbar()
|
tool.Blender.register_toolbar()
|
||||||
tool.Project.add_recent_ifc_project(self.get_filepath_abs())
|
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
|
metadata_filename = os.path.basename(output_file)[:-4] + suffix
|
||||||
else:
|
else:
|
||||||
metadata_filename = os.path.basename(output_file) + suffix
|
metadata_filename = os.path.basename(output_file) + suffix
|
||||||
|
|
||||||
if not tool.Project.get_metadata_document_information():
|
if not tool.Project.get_metadata_document_information():
|
||||||
tool.Project.create_metadata_document_information(metadata_filename)
|
tool.Project.create_metadata_document_information(metadata_filename)
|
||||||
else:
|
else:
|
||||||
@@ -3165,6 +3165,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
|||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
||||||
bl_idname = "bim.load_blend_metadata_and_ifc"
|
bl_idname = "bim.load_blend_metadata_and_ifc"
|
||||||
bl_label = "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.app.handlers.load_post.append(load_handler)
|
||||||
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Unused operator.
|
# TODO: Unused operator.
|
||||||
# Is there a need for this or 'DIR_PATH' propety subtype does almost the same,
|
# Is there a need for this or 'DIR_PATH' propety subtype does almost the same,
|
||||||
# but also has alt+click?
|
# but also has alt+click?
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ class BIMProperties(PropertyGroup):
|
|||||||
],
|
],
|
||||||
name="Time Unit",
|
name="Time Unit",
|
||||||
default="HOUR",
|
default="HOUR",
|
||||||
)
|
)
|
||||||
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
|
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
|
||||||
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
|
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
|
||||||
|
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ def update_drawing_name(
|
|||||||
camera = ifc.get_object(drawing)
|
camera = ifc.get_object(drawing)
|
||||||
if camera and camera.name != name:
|
if camera and camera.name != name:
|
||||||
camera.name = name
|
camera.name = name
|
||||||
|
|
||||||
group = drawing_tool.get_drawing_group(drawing)
|
group = drawing_tool.get_drawing_group(drawing)
|
||||||
if drawing_tool.get_name(group) != name:
|
if drawing_tool.get_name(group) != name:
|
||||||
ifc.run("attribute.edit_attributes", product=group, attributes={"Name": 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:
|
def _has_material_styles(ifc: type[tool.Ifc], element: ifcopenshell.entity_instance) -> bool:
|
||||||
"""Check if element has styles defined through its material.
|
"""Check if element has styles defined through its material.
|
||||||
|
|
||||||
Returns True if any constituent material has a style representation,
|
Returns True if any constituent material has a style representation,
|
||||||
which means styles should NOT be applied directly to the geometry.
|
which means styles should NOT be applied directly to the geometry.
|
||||||
"""
|
"""
|
||||||
materials = ifcopenshell.util.element.get_materials(element)
|
materials = ifcopenshell.util.element.get_materials(element)
|
||||||
|
|
||||||
if not materials:
|
if not materials:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check if any of the constituent materials have styles
|
# Check if any of the constituent materials have styles
|
||||||
for material in materials:
|
for material in materials:
|
||||||
if hasattr(material, 'HasRepresentation') and material.HasRepresentation:
|
if hasattr(material, "HasRepresentation") and material.HasRepresentation:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,17 +33,16 @@ def assign_type(
|
|||||||
element: ifcopenshell.entity_instance,
|
element: ifcopenshell.entity_instance,
|
||||||
type: ifcopenshell.entity_instance,
|
type: ifcopenshell.entity_instance,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
|
||||||
# Get the instance's current CardinalPoint before type assignment
|
# Get the instance's current CardinalPoint before type assignment
|
||||||
instance_cardinal_point = None
|
instance_cardinal_point = None
|
||||||
instance_material = ifcopenshell.util.element.get_material(element)
|
instance_material = ifcopenshell.util.element.get_material(element)
|
||||||
if instance_material and instance_material.is_a("IfcMaterialProfileSetUsage"):
|
if instance_material and instance_material.is_a("IfcMaterialProfileSetUsage"):
|
||||||
instance_cardinal_point = instance_material.CardinalPoint
|
instance_cardinal_point = instance_material.CardinalPoint
|
||||||
|
|
||||||
ifc.run("type.assign_type", related_objects=[element], relating_type=type)
|
ifc.run("type.assign_type", related_objects=[element], relating_type=type)
|
||||||
obj = ifc.get_object(element)
|
obj = ifc.get_object(element)
|
||||||
|
|
||||||
if type_tool.has_material_usage(element):
|
if type_tool.has_material_usage(element):
|
||||||
# Restore the instance's CardinalPoint to the new material usage
|
# Restore the instance's CardinalPoint to the new material usage
|
||||||
if instance_cardinal_point is not None:
|
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 and new_instance_material.is_a("IfcMaterialProfileSetUsage"):
|
||||||
if new_instance_material.CardinalPoint != instance_cardinal_point:
|
if new_instance_material.CardinalPoint != instance_cardinal_point:
|
||||||
new_instance_material.CardinalPoint = instance_cardinal_point
|
new_instance_material.CardinalPoint = instance_cardinal_point
|
||||||
|
|
||||||
# Force representation regeneration
|
# Force representation regeneration
|
||||||
from bonsai.bim.module.model.profile import DumbProfileRecalculator
|
from bonsai.bim.module.model.profile import DumbProfileRecalculator
|
||||||
|
|
||||||
DumbProfileRecalculator().recalculate([obj])
|
DumbProfileRecalculator().recalculate([obj])
|
||||||
# for now, representation regeneration handled by API listeners
|
# for now, representation regeneration handled by API listeners
|
||||||
else:
|
else:
|
||||||
type_data = type_tool.get_object_data(ifc.get_object(type))
|
type_data = type_tool.get_object_data(ifc.get_object(type))
|
||||||
if type_data:
|
if type_data:
|
||||||
type_tool.change_object_data(obj, type_data, is_global=False)
|
type_tool.change_object_data(obj, type_data, is_global=False)
|
||||||
|
|
||||||
type_tool.disable_editing(obj)
|
type_tool.disable_editing(obj)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -382,17 +382,14 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
assert isinstance(space, bpy.types.SpaceNodeEditor)
|
assert isinstance(space, bpy.types.SpaceNodeEditor)
|
||||||
if space.tree_type == "ShaderNodeTree":
|
if space.tree_type == "ShaderNodeTree":
|
||||||
context_override = {"area": area, "space": space, "screen": screen}
|
context_override = {"area": area, "space": space, "screen": screen}
|
||||||
|
|
||||||
# Add window if screen differs from current context
|
# Add window if screen differs from current context
|
||||||
context = bpy.context
|
context = bpy.context
|
||||||
if context and context.screen != screen:
|
if context and context.screen != screen:
|
||||||
window = next(
|
window = next((w for w in context.window_manager.windows if w.screen == screen), None)
|
||||||
(w for w in context.window_manager.windows if w.screen == screen),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
if window:
|
if window:
|
||||||
context_override["window"] = window
|
context_override["window"] = window
|
||||||
|
|
||||||
return context_override
|
return context_override
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1030,16 +1030,16 @@ class Loader(bonsai.core.tool.Loader):
|
|||||||
sense_factor = 1
|
sense_factor = 1
|
||||||
else:
|
else:
|
||||||
return mesh
|
return mesh
|
||||||
|
|
||||||
if len(layer_set.MaterialLayers) == 1:
|
if len(layer_set.MaterialLayers) == 1:
|
||||||
return mesh
|
return mesh
|
||||||
|
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
bm.from_mesh(mesh)
|
bm.from_mesh(mesh)
|
||||||
|
|
||||||
prev_co = None
|
prev_co = None
|
||||||
advance_direction = None # Will store direction to advance planes
|
advance_direction = None # Will store direction to advance planes
|
||||||
|
|
||||||
if not usage:
|
if not usage:
|
||||||
sense_factor = 1
|
sense_factor = 1
|
||||||
no = cls.get_extrusion_vector(element).normalized()
|
no = cls.get_extrusion_vector(element).normalized()
|
||||||
@@ -1047,7 +1047,7 @@ class Loader(bonsai.core.tool.Loader):
|
|||||||
advance_direction = no
|
advance_direction = no
|
||||||
elif usage.LayerSetDirection == "AXIS2":
|
elif usage.LayerSetDirection == "AXIS2":
|
||||||
co = Vector((0.0, offset, 0.0))
|
co = Vector((0.0, offset, 0.0))
|
||||||
|
|
||||||
# Get LOCAL extrusion direction
|
# Get LOCAL extrusion direction
|
||||||
local_extrusion = Vector([0.0, 0.0, 1.0])
|
local_extrusion = Vector([0.0, 0.0, 1.0])
|
||||||
if body := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW"):
|
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"):
|
if item.is_a("IfcExtrudedAreaSolid"):
|
||||||
local_extrusion = Vector(item.ExtrudedDirection.DirectionRatios).normalized()
|
local_extrusion = Vector(item.ExtrudedDirection.DirectionRatios).normalized()
|
||||||
break
|
break
|
||||||
|
|
||||||
# Thickness direction: perpendicular to extrusion and length
|
# Thickness direction: perpendicular to extrusion and length
|
||||||
thickness_dir = local_extrusion.cross(Vector([1.0, 0.0, 0.0])).normalized()
|
thickness_dir = local_extrusion.cross(Vector([1.0, 0.0, 0.0])).normalized()
|
||||||
|
|
||||||
# Ensure it points in POSITIVE Y (through wall thickness, not backwards)
|
# Ensure it points in POSITIVE Y (through wall thickness, not backwards)
|
||||||
if thickness_dir.y < 0:
|
if thickness_dir.y < 0:
|
||||||
thickness_dir = -thickness_dir
|
thickness_dir = -thickness_dir
|
||||||
|
|
||||||
no = thickness_dir
|
no = thickness_dir
|
||||||
advance_direction = thickness_dir
|
advance_direction = thickness_dir
|
||||||
elif usage.LayerSetDirection == "AXIS3":
|
elif usage.LayerSetDirection == "AXIS3":
|
||||||
@@ -1077,10 +1077,10 @@ class Loader(bonsai.core.tool.Loader):
|
|||||||
no = cls.get_extrusion_vector(element).normalized()
|
no = cls.get_extrusion_vector(element).normalized()
|
||||||
no = Vector([1.0, 0.0, 0.0])
|
no = Vector([1.0, 0.0, 0.0])
|
||||||
advance_direction = no
|
advance_direction = no
|
||||||
|
|
||||||
no *= sense_factor
|
no *= sense_factor
|
||||||
advance_direction *= sense_factor
|
advance_direction *= sense_factor
|
||||||
|
|
||||||
# Cache this
|
# Cache this
|
||||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||||
styles = {}
|
styles = {}
|
||||||
@@ -1088,25 +1088,25 @@ class Loader(bonsai.core.tool.Loader):
|
|||||||
for i, material in enumerate(mesh.materials):
|
for i, material in enumerate(mesh.materials):
|
||||||
if style := tool.Ifc.get_entity(material):
|
if style := tool.Ifc.get_entity(material):
|
||||||
styles[style] = i
|
styles[style] = i
|
||||||
|
|
||||||
last_i = len(layer_set.MaterialLayers) - 1
|
last_i = len(layer_set.MaterialLayers) - 1
|
||||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||||
if i != last_i:
|
if i != last_i:
|
||||||
prev_co = co.copy()
|
prev_co = co.copy()
|
||||||
# Use advance_direction (not no) to move planes!
|
# Use advance_direction (not no) to move planes!
|
||||||
co += advance_direction * layer.LayerThickness * cls.unit_scale
|
co += advance_direction * layer.LayerThickness * cls.unit_scale
|
||||||
|
|
||||||
bisect_geom = bmesh.ops.bisect_plane(
|
bisect_geom = bmesh.ops.bisect_plane(
|
||||||
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
|
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"])
|
bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"])
|
||||||
|
|
||||||
if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)):
|
if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)):
|
||||||
continue
|
continue
|
||||||
if (material_index := styles.get(style, None)) is None:
|
if (material_index := styles.get(style, None)) is None:
|
||||||
material_index = len(mesh.materials)
|
material_index = len(mesh.materials)
|
||||||
mesh.materials.append(tool.Ifc.get_object(style))
|
mesh.materials.append(tool.Ifc.get_object(style))
|
||||||
|
|
||||||
if i == last_i:
|
if i == last_i:
|
||||||
for face in bisect_geom["geom"]:
|
for face in bisect_geom["geom"]:
|
||||||
if isinstance(face, bmesh.types.BMFace):
|
if isinstance(face, bmesh.types.BMFace):
|
||||||
@@ -1139,14 +1139,14 @@ class Loader(bonsai.core.tool.Loader):
|
|||||||
item = item.FirstOperand
|
item = item.FirstOperand
|
||||||
if item.is_a("IfcExtrudedAreaSolid"):
|
if item.is_a("IfcExtrudedAreaSolid"):
|
||||||
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
|
local_direction = Vector(item.ExtrudedDirection.DirectionRatios)
|
||||||
|
|
||||||
# Transform to world coordinates using object rotation
|
# Transform to world coordinates using object rotation
|
||||||
obj = tool.Ifc.get_object(element)
|
obj = tool.Ifc.get_object(element)
|
||||||
if obj:
|
if obj:
|
||||||
# Apply object rotation to get actual world direction
|
# Apply object rotation to get actual world direction
|
||||||
world_direction = obj.matrix_world.to_3x3() @ local_direction
|
world_direction = obj.matrix_world.to_3x3() @ local_direction
|
||||||
return world_direction
|
return world_direction
|
||||||
|
|
||||||
return local_direction
|
return local_direction
|
||||||
return Vector([0.0, 0.0, 1.0])
|
return Vector([0.0, 0.0, 1.0])
|
||||||
|
|
||||||
|
|||||||
@@ -620,12 +620,11 @@ class Model(bonsai.core.tool.Model):
|
|||||||
if not openings[i].obj:
|
if not openings[i].obj:
|
||||||
openings.remove(i)
|
openings.remove(i)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def save_custom_offset_to_pset(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
|
def save_custom_offset_to_pset(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
|
||||||
"""Save custom offset settings to BBIM_MaterialLayer pset."""
|
"""Save custom offset settings to BBIM_MaterialLayer pset."""
|
||||||
props = tool.Material.get_object_material_props(obj)
|
props = tool.Material.get_object_material_props(obj)
|
||||||
|
|
||||||
if not props.use_custom_offset:
|
if not props.use_custom_offset:
|
||||||
# Remove pset if custom offset is disabled
|
# Remove pset if custom offset is disabled
|
||||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
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"])
|
pset_entity = tool.Ifc.get().by_id(pset["id"])
|
||||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset_entity)
|
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset_entity)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine which reference to save based on usage type
|
# Determine which reference to save based on usage type
|
||||||
usage_type = tool.Model.get_usage_type(element)
|
usage_type = tool.Model.get_usage_type(element)
|
||||||
custom_wall_reference = None
|
custom_wall_reference = None
|
||||||
custom_slab_reference = None
|
custom_slab_reference = None
|
||||||
|
|
||||||
if usage_type == "LAYER2":
|
if usage_type == "LAYER2":
|
||||||
custom_wall_reference = props.custom_wall_reference
|
custom_wall_reference = props.custom_wall_reference
|
||||||
elif usage_type == "LAYER3":
|
elif usage_type == "LAYER3":
|
||||||
custom_slab_reference = props.custom_slab_reference
|
custom_slab_reference = props.custom_slab_reference
|
||||||
|
|
||||||
# Get or create pset
|
# Get or create pset
|
||||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
||||||
if pset_data:
|
if pset_data:
|
||||||
pset = tool.Ifc.get().by_id(pset_data["id"])
|
pset = tool.Ifc.get().by_id(pset_data["id"])
|
||||||
else:
|
else:
|
||||||
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="BBIM_MaterialLayer")
|
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="BBIM_MaterialLayer")
|
||||||
|
|
||||||
# Save properties (store in SI units)
|
# Save properties (store in SI units)
|
||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
properties = {
|
properties = {
|
||||||
@@ -667,14 +666,14 @@ class Model(bonsai.core.tool.Model):
|
|||||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
||||||
if not pset:
|
if not pset:
|
||||||
return
|
return
|
||||||
|
|
||||||
props = tool.Material.get_object_material_props(obj)
|
props = tool.Material.get_object_material_props(obj)
|
||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
# Load properties
|
# Load properties
|
||||||
props.use_custom_offset = pset.get("UseCustomOffset", False)
|
props.use_custom_offset = pset.get("UseCustomOffset", False)
|
||||||
props.custom_offset = pset.get("CustomOffset", 0.0) * unit_scale # Convert from SI
|
props.custom_offset = pset.get("CustomOffset", 0.0) * unit_scale # Convert from SI
|
||||||
|
|
||||||
# Load the appropriate reference based on usage type
|
# Load the appropriate reference based on usage type
|
||||||
usage_type = tool.Model.get_usage_type(element)
|
usage_type = tool.Model.get_usage_type(element)
|
||||||
if usage_type == "LAYER2":
|
if usage_type == "LAYER2":
|
||||||
@@ -718,14 +717,16 @@ class Model(bonsai.core.tool.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Get custom offset value, reading from pset if props are not set."""
|
||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
layer_params = tool.Model.get_material_layer_parameters(element)
|
layer_params = tool.Model.get_material_layer_parameters(element)
|
||||||
layer_offset = layer_params["offset"]
|
layer_offset = layer_params["offset"]
|
||||||
thickness = layer_params["thickness"] / unit_scale
|
thickness = layer_params["thickness"] / unit_scale
|
||||||
props = tool.Material.get_object_material_props(obj)
|
props = tool.Material.get_object_material_props(obj)
|
||||||
|
|
||||||
# Try to load from pset if not already in props
|
# Try to load from pset if not already in props
|
||||||
if not props.use_custom_offset:
|
if not props.use_custom_offset:
|
||||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
pset = ifcopenshell.util.element.get_pset(element, "BBIM_MaterialLayer")
|
||||||
@@ -733,7 +734,7 @@ class Model(bonsai.core.tool.Model):
|
|||||||
# Load from pset
|
# Load from pset
|
||||||
custom_offset = pset.get("CustomOffset", 0.0)
|
custom_offset = pset.get("CustomOffset", 0.0)
|
||||||
usage_type = tool.Model.get_usage_type(element)
|
usage_type = tool.Model.get_usage_type(element)
|
||||||
|
|
||||||
if usage_type == "LAYER2":
|
if usage_type == "LAYER2":
|
||||||
custom_offset_reference = pset.get("CustomWallReference", "CENTER")
|
custom_offset_reference = pset.get("CustomWallReference", "CENTER")
|
||||||
elif usage_type == "LAYER3":
|
elif usage_type == "LAYER3":
|
||||||
@@ -753,7 +754,7 @@ class Model(bonsai.core.tool.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
direction_sense = layer_params["direction_sense"]
|
direction_sense = layer_params["direction_sense"]
|
||||||
|
|
||||||
if direction_sense == "POSITIVE" and custom_offset_reference in {"INTERIOR", "TOP"}:
|
if direction_sense == "POSITIVE" and custom_offset_reference in {"INTERIOR", "TOP"}:
|
||||||
layer_offset = custom_offset - thickness * unit_scale
|
layer_offset = custom_offset - thickness * unit_scale
|
||||||
if direction_sense == "POSITIVE" and custom_offset_reference in {"CENTER", "MIDDLE"}:
|
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()
|
ifc_file = tool.Ifc.get()
|
||||||
if not ifc_file:
|
if not ifc_file:
|
||||||
raise Exception("No IFC file loaded")
|
raise Exception("No IFC file loaded")
|
||||||
|
|
||||||
doc = tool.Ifc.run("document.add_information", parent=None)
|
doc = tool.Ifc.run("document.add_information", parent=None)
|
||||||
|
|
||||||
if ifc_file.schema == "IFC2X3":
|
if ifc_file.schema == "IFC2X3":
|
||||||
tool.Ifc.run("document.edit_information", information=doc, attributes={
|
tool.Ifc.run(
|
||||||
"DocumentId": "BLEND_METADATA",
|
"document.edit_information",
|
||||||
"Name": "Blend Metadata",
|
information=doc,
|
||||||
"Scope": "BLEND_METADATA",
|
attributes={
|
||||||
"Description": "References to blend metadata file for this IFC project",
|
"DocumentId": "BLEND_METADATA",
|
||||||
"Location": metadata_filename
|
"Name": "Blend Metadata",
|
||||||
})
|
"Scope": "BLEND_METADATA",
|
||||||
|
"Description": "References to blend metadata file for this IFC project",
|
||||||
|
"Location": metadata_filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
tool.Ifc.run("document.edit_information", information=doc, attributes={
|
tool.Ifc.run(
|
||||||
"Identification": "BLEND_METADATA",
|
"document.edit_information",
|
||||||
"Name": "Blend Metadata",
|
information=doc,
|
||||||
"Scope": "BLEND_METADATA",
|
attributes={
|
||||||
"Description": "References to blend metadata file for this IFC project",
|
"Identification": "BLEND_METADATA",
|
||||||
"Location": metadata_filename
|
"Name": "Blend Metadata",
|
||||||
})
|
"Scope": "BLEND_METADATA",
|
||||||
|
"Description": "References to blend metadata file for this IFC project",
|
||||||
|
"Location": metadata_filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -556,14 +564,12 @@ class Project(bonsai.core.tool.Project):
|
|||||||
doc = cls.get_metadata_document_information()
|
doc = cls.get_metadata_document_information()
|
||||||
if not doc:
|
if not doc:
|
||||||
return
|
return
|
||||||
|
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
if not ifc_file:
|
if not ifc_file:
|
||||||
return
|
return
|
||||||
|
|
||||||
tool.Ifc.run("document.edit_information", information=doc, attributes={
|
tool.Ifc.run("document.edit_information", information=doc, attributes={"Location": metadata_filename})
|
||||||
"Location": metadata_filename
|
|
||||||
})
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remove_metadata_document_information(cls) -> None:
|
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
|
break
|
||||||
if inches is None:
|
if inches is None:
|
||||||
inches = 0
|
inches = 0
|
||||||
|
|
||||||
# If feet is negative, inches should also be negative (subtractive)
|
# If feet is negative, inches should also be negative (subtractive)
|
||||||
if feet < 0:
|
if feet < 0:
|
||||||
inches = -inches
|
inches = -inches
|
||||||
|
|
||||||
# Convert to meters
|
# Convert to meters
|
||||||
total_meters = (feet * 0.3048) + (inches * 0.0254)
|
total_meters = (feet * 0.3048) + (inches * 0.0254)
|
||||||
return total_meters
|
return total_meters
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import bonsai, json, bpy
|
import bonsai, json, bpy
|
||||||
from pathlib import Path
|
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_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||||
settings.update({
|
settings.update(
|
||||||
"bonsai.localRoot": repo_path.as_posix(),
|
{
|
||||||
"bonsai.remoteRoot": install_path.as_posix(),
|
"bonsai.localRoot": repo_path.as_posix(),
|
||||||
"bonsai.blenderPath": Path(bpy.app.binary_path).parent.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)
|
json_data = json.dumps(settings, indent=2)
|
||||||
|
|
||||||
settings_path.write_text(json_data)
|
settings_path.write_text(json_data)
|
||||||
|
|
||||||
print("\n\nBonsai/VSCode development environment configured successfully!\n\n")
|
print("\n\nBonsai/VSCode development environment configured successfully!\n\n")
|
||||||
|
|
||||||
|
|||||||
@@ -107,23 +107,20 @@ class Usecase:
|
|||||||
for p in self.polyline
|
for p in self.polyline
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
points = [
|
points = [(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1])) for p in self.polyline]
|
||||||
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1]))
|
|
||||||
for p in self.polyline
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.file.schema == "IFC2X3":
|
if self.file.schema == "IFC2X3":
|
||||||
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
|
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
|
||||||
else:
|
else:
|
||||||
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points))
|
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points))
|
||||||
|
|
||||||
if self.x_angle:
|
if self.x_angle:
|
||||||
direction_ratios = (0.0, sin(self.x_angle), cos(self.x_angle))
|
direction_ratios = (0.0, sin(self.x_angle), cos(self.x_angle))
|
||||||
else:
|
else:
|
||||||
direction_ratios = (0.0, 0.0, 1.0)
|
direction_ratios = (0.0, 0.0, 1.0)
|
||||||
|
|
||||||
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
||||||
|
|
||||||
# Calculate depth based on extrusion angle
|
# Calculate depth based on extrusion angle
|
||||||
extrusion_angle = abs(self.x_angle) if self.x_angle else 0
|
extrusion_angle = abs(self.x_angle) if self.x_angle else 0
|
||||||
if extrusion_angle > 1e-6:
|
if extrusion_angle > 1e-6:
|
||||||
@@ -132,7 +129,7 @@ class Usecase:
|
|||||||
else:
|
else:
|
||||||
perpendicular_depth = self.convert_si_to_unit(self.depth)
|
perpendicular_depth = self.convert_si_to_unit(self.depth)
|
||||||
perpendicular_offset = self.convert_si_to_unit(self.offset)
|
perpendicular_offset = self.convert_si_to_unit(self.offset)
|
||||||
|
|
||||||
position = None
|
position = None
|
||||||
if self.file.schema == "IFC2X3" or self.offset != 0:
|
if self.file.schema == "IFC2X3" or self.offset != 0:
|
||||||
position_vector = (
|
position_vector = (
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class FormatTransformer(lark.Transformer):
|
|||||||
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
|
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
|
||||||
if self.element is None:
|
if self.element is None:
|
||||||
return "0" # Default value if no element context
|
return "0" # Default value if no element context
|
||||||
|
|
||||||
query_path = args[0]
|
query_path = args[0]
|
||||||
try:
|
try:
|
||||||
value = get_element_value(self.element, query_path)
|
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:
|
def format(query: str, element: Optional[ifcopenshell.entity_instance] = None) -> str:
|
||||||
"""Format a query string with optional element context for variable substitution.
|
"""Format a query string with optional element context for variable substitution.
|
||||||
|
|
||||||
:param query: Format query string (can include {{variable}} placeholders)
|
:param query: Format query string (can include {{variable}} placeholders)
|
||||||
:param element: Optional IFC element for variable substitution
|
:param element: Optional IFC element for variable substitution
|
||||||
:return: Formatted string
|
:return: Formatted string
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
format("{{z}} / 2", element) # Substitutes element's z value
|
format("{{z}} / 2", element) # Substitutes element's z value
|
||||||
format("imperial_length({{z}} / 2, 4)", element) # Uses z in calculation
|
format("imperial_length({{z}} / 2, 4)", element) # Uses z in calculation
|
||||||
@@ -1257,4 +1257,4 @@ class FacetTransformer(lark.Transformer):
|
|||||||
|
|
||||||
if comparison.startswith("!"):
|
if comparison.startswith("!"):
|
||||||
return not result
|
return not result
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user