Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2026-03-16 07:00:17 +01:00
13 changed files with 199 additions and 63 deletions
+1
View File
@@ -83,6 +83,7 @@ class BIM_PT_bsdd(Panel):
row = self.layout.row()
row.operator("bim.load_bsdd_dictionaries")
class BIM_UL_bsdd_dictionaries(UIList):
def draw_item(
self,
@@ -37,6 +37,7 @@ messages = {
class CadTrimExtend(bpy.types.Operator):
bl_idname = "bim.cad_trim_extend"
bl_label = "CAD Trim / Extend"
bl_description = "Extends/reduces element to 3D cursor"
@classmethod
def poll(cls, context):
@@ -82,6 +83,7 @@ class CadTrimExtend(bpy.types.Operator):
class CadMitre(bpy.types.Operator):
bl_idname = "bim.cad_mitre"
bl_label = "CAD Mitre"
bl_description = "Joins two non-parallel paths at their intersection"
@classmethod
def poll(cls, context):
+59 -21
View File
@@ -106,23 +106,37 @@ class CadTool(WorkSpaceTool):
)
row = layout.row(align=True)
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
add_layout_hotkey_operator(
row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__.split("\n", 1)[1].strip(), ui_context
)
elif (
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
@@ -132,15 +146,21 @@ class CadTool(WorkSpaceTool):
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
)
row = layout.row(align=True)
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
)
else:
if (
@@ -168,19 +188,37 @@ class CadTool(WorkSpaceTool):
add_layout_hotkey_operator(row, "Set Gable Roof Angle", "S_R", "Set Gable Roof Angle", ui_context)
row = layout.row(align=True)
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
add_layout_hotkey_operator(
row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
add_layout_hotkey_operator(
row,
"2-Point Arc",
"S_C",
bpy.ops.bim.cad_arc_from_2_points.__doc__.split("\n", 1)[1].strip(),
ui_context,
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(
row,
"3-Point Arc",
"S_V",
bpy.ops.bim.cad_arc_from_3_points.__doc__.split("\n", 1)[1].strip(),
ui_context,
)
class CadHotkey(bpy.types.Operator):
@@ -3364,57 +3364,55 @@ class OrderTextLiteralDown(bpy.types.Operator):
return {"FINISHED"}
# Ifc Operator is unnecessary, because suboperator is handling IFC changes.
class AssignSelectedObjectAsProduct(bpy.types.Operator):
class AssignSelectedObjectAsProduct(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_selected_as_product"
bl_label = "Assign Selected Object As Product"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if len(context.selected_objects) != 2:
cls.poll_message_set("2 objects need to be selected")
if len(context.selected_objects) < 2:
cls.poll_message_set("At least 2 objects need to be selected")
return False
return True
def execute(self, context):
assert bpy.context.view_layer
def _execute(self, context):
objs = context.selected_objects[:]
obj1, obj2 = objs
element1 = tool.Ifc.get_entity(obj1)
element2 = tool.Ifc.get_entity(obj2)
assert element1 and element2
ifc_objs = [(o, tool.Ifc.get_entity(o)) for o in objs if tool.Ifc.get_entity(o)]
# Check if at least one object is an IfcAnnotation
is_annotation1 = element1.is_a("IfcAnnotation")
is_annotation2 = element2.is_a("IfcAnnotation")
annotations = [(o, e) for o, e in ifc_objs if e.is_a("IfcAnnotation")]
non_annotations = [(o, e) for o, e in ifc_objs if not e.is_a("IfcAnnotation")]
if not (is_annotation1 or is_annotation2):
self.report({"ERROR"}, "At least one of the selected objects must be IfcAnnotation.")
if not annotations:
self.report({"ERROR"}, "At least one selected object must be an IfcAnnotation.")
return {"CANCELLED"}
# If both are annotations, use the currently active object as relating product
if is_annotation1 and is_annotation2:
if len(non_annotations) == 1:
# One product, one or more annotations — assign all annotations to the product.
product = non_annotations[0][1]
elif len(non_annotations) == 0 and len(annotations) == 2:
# Both objects are annotations — use the non-active one as the relating product.
active_obj = context.active_object
if active_obj == obj1:
other_selected_object = obj1
bpy.context.view_layer.objects.active = obj2
if annotations[0][0] == active_obj:
annotation_obj, annotation = annotations[0]
product = annotations[1][1]
else:
other_selected_object = obj2
bpy.context.view_layer.objects.active = obj1
# If only one is an annotation, make it the active object
elif is_annotation1:
other_selected_object = obj2
bpy.context.view_layer.objects.active = obj1
annotation_obj, annotation = annotations[1]
product = annotations[0][1]
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
tool.Blender.update_viewport()
return
else:
other_selected_object = obj1
bpy.context.view_layer.objects.active = obj2
self.report(
{"ERROR"},
"Select exactly one product object and one or more IfcAnnotation objects.",
)
return {"CANCELLED"}
assert (active_obj := context.active_object)
props = tool.Drawing.get_object_assigned_product_props(active_obj)
props.relating_product = other_selected_object
bpy.ops.bim.edit_assigned_product()
return {"FINISHED"}
for annotation_obj, _ in annotations:
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product)
tool.Blender.update_viewport()
class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
@@ -3886,8 +3884,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
ifc_file = tool.Ifc.get()
params = {"check_existing": False}
image = load_image(abs_path.name, str(abs_path.parent), **params)
image = load_image(abs_path.name, str(abs_path.parent), check_existing=False)
mesh = bpy.data.meshes.new(image_filepath.stem)
obj = bpy.data.objects.new(image_filepath.stem, mesh)
+1 -1
View File
@@ -82,7 +82,7 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_grid"
bl_idname = "bim.add_grid"
bl_label = "Grid"
bl_description = "Add IfcGrid."
bl_options = {"REGISTER", "UNDO"}
@@ -76,6 +76,7 @@ classes = (
operator.UnlinkIfc,
operator.UnloadLink,
workspace.ExploreHotkey,
workspace.GenerateUVMap,
prop.LibraryBreadcrumb,
prop.LibraryElement,
prop.FilterCategory,
@@ -87,6 +87,9 @@ class ExploreTool(bpy.types.WorkSpaceTool):
op.hotkey = "S_S"
op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them"
op = row.operator("bim.generate_uv_map", text="Generate UV Map", icon="UV")
op.description = "Generate UV map for selected mesh."
class ExploreHotkey(bpy.types.Operator):
bl_idname = "bim.explore_hotkey"
@@ -153,3 +156,20 @@ class ExploreHotkey(bpy.types.Operator):
def hotkey_A_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
class GenerateUVMap(bpy.types.Operator):
bl_idname = "bim.generate_uv_map"
bl_label = "Generate UV Map"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
description: bpy.props.StringProperty()
def execute(self, context):
obj = context.active_object
if not obj or not hasattr(obj, "data") or not hasattr(obj.data, "polygons"):
self.report({"ERROR"}, "No valid mesh selected.")
return {"CANCELLED"}
tool.Loader.load_generated_uv_map(obj.data)
self.report({"INFO"}, "Generated UV map for selected mesh.")
return {"FINISHED"}
+1 -1
View File
@@ -251,7 +251,7 @@ class BIM_PT_grids(Panel):
bl_options = {"HEADER_LAYOUT_EXPAND"}
def draw(self, context):
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
self.layout.row().operator("bim.add_grid", icon="ADD", text="Add Grids")
def draw_header(self, context):
props = tool.Spatial.get_grid_props()
+3 -1
View File
@@ -143,7 +143,9 @@ def assign_material(
material_tool.add_material_to_set(material_set=material, material=default_material)
elif material_tool.is_a_material_set(assigned_material):
material_tool.add_material_to_set(material_set=assigned_material, material=material)
material_tool.ensure_material_assigned(elements=[element], material_type=element_material_type, material=material)
material_tool.ensure_material_assigned(
elements=[element], material_type=element_material_type, material=material
)
def unassign_material(ifc: type[tool.Ifc], material_tool: type[tool.Material], objects: list[bpy.types.Object]) -> None:
+76 -1
View File
@@ -310,9 +310,84 @@ Scenario: Create sheet - with a drawing added to it
When I click "OUTPUT"
Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall"
Scenario: Enable editing text
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I save IFC project
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the object "IfcAnnotation/TEXT" is selected
And I look at the "BIM_PT_text" panel
When I click "Enable Editing Text"
Then I see "Literals:"
And I don't see "FontSize"
Scenario: Disable editing text
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I save IFC project
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the object "IfcAnnotation/TEXT" is selected
And I look at the "BIM_PT_text" panel
And I click "Enable Editing Text"
When I click "CANCEL"
Then I see "FontSize"
And I don't see "Literals:"
Scenario: Edit text - no changes
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I save IFC project
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the object "IfcAnnotation/TEXT" is selected
And I look at the "BIM_PT_text" panel
And I click "Enable Editing Text"
When I click "Edit Text"
Then I see "FontSize"
And I don't see "Literals:"
Scenario: Edit text - change literal
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I save IFC project
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the object "IfcAnnotation/TEXT" is selected
And I look at the "BIM_PT_text" panel
And I click "Enable Editing Text"
And I set the "Literal" property to "Hello World"
When I click "Edit Text"
Then I see "Hello World"
Scenario: Add reference image
Given an empty IFC project
And I save IFC project
When I press "bim.add_reference_image(filepath='{cwd}/test/files/image.jpg')"
When I press "bim.add_reference_image(filepath='{cwd}/test/files/image.jpg', x_length=1, y_length=0.565)"
Then the object "IfcAnnotation/image" exists
And the object "IfcAnnotation/image" dimensions are "1.0,0.565,0."
+1 -1
View File
@@ -57,7 +57,7 @@ Scenario: Add one type from the Construction Type Browser
Scenario: Add grid
Given an empty IFC project
When I press "mesh.add_grid"
When I press "bim.add_grid"
Then the object "IfcGrid/Grid" is an "IfcGrid"
And the object "IfcGridAxis/A" is an "IfcGridAxis"
And the object "IfcGridAxis/B" is an "IfcGridAxis"
+1 -1
View File
@@ -914,7 +914,7 @@ Scenario: Export IFC - with moved object location synchronised
Scenario: Export IFC - with moved grid axis location synchronised
Given an empty IFC project
And I press "mesh.add_grid"
And I press "bim.add_grid"
When the object "IfcGridAxis/01" is moved to "1,0,0"
And I save IFC project
And I load previously saved IFC project
+1 -1
View File
@@ -378,7 +378,7 @@ def i_look_at_the_panel_panel(panel: str) -> None:
# Option to provide explicit panel name if panel names overlap.
panel_class = getattr(bpy.types, panel, None)
if panel_class is None:
if panel_class is None or panel_class.bl_rna.base.name not in ("Panel", "Operator", "Menu", "UIList"):
global ui_name_cache
create_ui_name_cache()
if panel not in ui_name_cache: