Compare commits

...

28 Commits

Author SHA1 Message Date
Dion Moult 60ae4e80ce New get_contained util.element function 2024-06-05 13:04:50 +10:00
Dion Moult 05ec36a37d Spatial manager now shows aggregated objects inside selected space 2024-06-05 13:04:35 +10:00
Andrej730 dc8ea31138 Bump ladybug tools build in docs 2024-06-04 13:42:33 +05:00
Dion Moult faaea7db49 Spatial manager now allows inline editing of names and elevations for a slicker UI 2024-06-04 15:10:28 +10:00
Dion Moult b14ff94303 Spatial manager renamed to project tree and added icons 2024-06-04 13:57:57 +10:00
Dion Moult b3dcfbf0dc See #4796. Fix issue where orphaned aggregate is never shown when loading IFC. 2024-06-04 12:31:07 +10:00
Ryan Schultz b68109d32b small fix for #4745 2024-06-03 12:31:03 -05:00
Dion Moult 2ea3fe4ab9 Fix qto bug where cubic meter units are incorrectly converted. 2024-06-03 22:49:40 +10:00
Andrej730 94c159ccd0 Consider elements with openings reloading representations
Before - https://imgur.com/a/MMnGMBo .
Occurrences with openings wasn't reloaded if you'd change their representation items (add/remove a style, remove a representation item). Also type/occurrences representations were not always reloading.

After - https://imgur.com/a/vBDejb8

Similar thing will be added to switch representation and to representation update to solve issues like this - https://imgur.com/a/0DFIBj2 (issue is still present now).
2024-06-03 17:47:37 +05:00
Andrej730 e4eead6662 styles ui - bim.assign_style_to_selected
Just a simple operator to explicitly assign selected style to the selected objects from Styles UI

https://imgur.com/a/JOLVPz7
2024-06-03 17:44:06 +05:00
Andrej730 ee4e92dfa3 remove bim.update_current_style poll
as this operator can be executed without any objects selected
2024-06-03 17:44:05 +05:00
Andrej730 f4ce2965f9 fix error saving ifc file / assigning classes 6eb3c4814 2024-06-03 17:44:05 +05:00
Andrej730 5fb7e6e375 resource.remove_resource - fix issue removing resources in ifc2x3 2024-06-03 17:44:05 +05:00
Andrej730 c399eb259a resource.add_resource_quantity to ensure quantity type is supported 2024-06-03 17:44:05 +05:00
Andrej730 5ccdf187d5 change type of exception when setting non-optional property to TypeError
TypeError is more correct since error occurs when user is trying to set some attribute value with None while atttribute doesn't support None type. ValueError is typically raised when provided value has a correct type but unsupported value.
2024-06-03 17:44:05 +05:00
Andrej730 d53351d218 Remove redundant description repeating the label #4771 2024-06-03 17:44:05 +05:00
Andrej730 08a82c7b57 remove dead code after #4745 2024-06-03 17:44:05 +05:00
Andrej730 499cdbb9b8 bump old api argument deprecation date 2024-06-03 17:44:05 +05:00
Andrej730 4a5d1eba0b specify exported api methods in __all__
It's py.types requirement, similar to 6e2edbf

Example issue without __all__:
import ifcopenshell
import ifcopenshell.api.project

# "create_file" is not exported from module "ifcopenshell.api.project"
ifcopenshell.api.project.create_file()
2024-06-03 17:44:05 +05:00
Andrej730 70091aa28b typing 2024-06-03 17:44:04 +05:00
Manu Varkey 3d566cc401 Fix missing code in pull request #4784 (#4794)
One line of code was inadvertently missed out in the pull request https://github.com/IfcOpenShell/IfcOpenShell/pull/4784
2024-06-03 21:30:58 +10:00
Dion Moult 6eb3c48144 Fix #3973. Fix #4787. Clarify that you cannot have aggregated physical elements where the whole has a body representation. 2024-06-03 15:37:24 +10:00
Dion Moult 44d180007e Migrate test qto tool to new qto calculator 2024-06-03 15:16:27 +10:00
Dion Moult 7e8060b224 Fix #4791. Hide irrelevant spatial containment panel for spatial elements. 2024-06-03 14:32:01 +10:00
Dion Moult d408568d01 Fix #4793. Bug where autodetected patch UI failed with new signature typing. 2024-06-03 14:08:54 +10:00
Ryan Schultz 47704b1625 Keeps selected objects selected when switching from one drawing to an… (#4745)
Keeps selected objects selected when switching from one drawing to another. As discussed: proposed: https://community.osarch.org/discussion/2179/not-unselecting-objects-when-switching-from-one-drawing-to-the-other#latest
2024-06-02 09:05:38 -05:00
Dion Moult 23fee9c8c8 Bring back formwork calculation feature that uses remesh 2024-06-02 17:43:25 +10:00
Dion Moult c5d1801e1e Fix #4786. Speed up parent filter. 2024-06-02 17:39:23 +10:00
78 changed files with 1036 additions and 408 deletions
@@ -142,6 +142,7 @@ classes = [
ui.BIM_PT_tabs,
# Project overview
ui.BIM_PT_tab_project_info,
ui.BIM_PT_tab_project_tree,
ui.BIM_PT_tab_project_setup,
ui.BIM_PT_tab_geometry,
ui.BIM_PT_tab_stakeholders,
@@ -1702,6 +1702,7 @@ class IfcImporter:
if aggregate["element"].is_a("IfcElementType"):
self.type_collection.children.link(aggregate["collection"])
continue
self.project["blender"].children.link(aggregate["collection"])
def create_materials(self) -> None:
for material in self.file.by_type("IfcMaterial"):
@@ -55,21 +55,24 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, Operator):
if not relating_obj:
return
for obj in bpy.context.selected_objects + [bpy.context.active_object]:
for obj in tool.Blender.get_selected_objects():
if obj == relating_obj:
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
result = core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
if not result:
self.report({"ERROR"}, f" Cannot aggregate {obj.name} to {relating_obj.name}")
try:
core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
except core.IncompatibleAggregateError:
self.report({"ERROR"}, f"Cannot aggregate {obj.name} to {relating_obj.name}")
except core.AggregateRepresentationError:
self.report({"ERROR"}, f"Cannot aggregate to {relating_obj.name} with a body representation")
class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
@@ -29,13 +29,9 @@ def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.CoveringTool, after={"bim.structural_tool"}, separator=False, group=False)
bpy.types.Scene.BIMCoveringProperties = bpy.props.PointerProperty(type=prop.BIMCoveringProperties)
# bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
# bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.CoveringTool)
del bpy.types.Scene.BIMCoveringProperties
# del bpy.types.Object.BIMObjectSpatialProperties
# del bpy.types.Scene.BIMSpatialManagerProperties
@@ -29,6 +29,7 @@ import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
@@ -1537,14 +1538,7 @@ class ActivateDrawing(bpy.types.Operator):
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
try:
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
except core.CameraNotAvailableError:
self.report(
{"ERROR"},
"The drawing view is not available. Ensure you have not excluded it in the active view layer.",
)
return {"CANCELLED"}
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
@@ -1638,7 +1632,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
camera_props = context.active_object.data.BIMCameraProperties
camera_props = context.scene.camera.data.BIMCameraProperties
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
@@ -596,6 +596,7 @@ class EditMaterialSetItemProfile(bpy.types.Operator, tool.Ifc.Operator):
attributes = blenderbim.bim.helper.export_attributes(self.props.material_set_item_profile_attributes)
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes)
self.props.active_material_set_item_id = 0
self.props.material_set_item_profile_attributes.clear()
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
@@ -702,7 +703,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
class ExpandMaterialCategory(bpy.types.Operator):
bl_idname = "bim.expand_material_category"
bl_label = "Expand Material Category"
bl_description = "Expand material category.\n\nSHIFT+CLICK to expand all material categories"
bl_description = "SHIFT+CLICK to expand all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
@@ -731,7 +732,7 @@ class ExpandMaterialCategory(bpy.types.Operator):
class ContractMaterialCategory(bpy.types.Operator):
bl_idname = "bim.contract_material_category"
bl_label = "Contract Material Category"
bl_description = "Contract material category.\n\nSHIFT+CLICK to contract all material categories"
bl_description = "SHIFT+CLICK to contract all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"})
@@ -121,12 +121,16 @@ class UpdateIfcPatchArguments(bpy.types.Operator):
for arg_name in inputs:
arg_info = inputs[arg_name]
new_attr = patch_args.add()
data_type = arg_info.get("type", "str")
if isinstance(data_type, list):
data_type = [dt for dt in data_type if dt != "NoneType"][0]
new_attr.data_type = {
"Literal": "string",
"str": "string",
"float": "float",
"int": "integer",
"bool": "boolean",
}[arg_info.get("type", "str")]
}[data_type]
new_attr.name = arg_name
new_attr.set_value(arg_info.get("default", new_attr.get_value_default()))
return {"FINISHED"}
@@ -23,6 +23,7 @@ classes = (
operator.CalculateCircleRadius,
operator.CalculateEdgeLengths,
operator.CalculateFaceAreas,
operator.CalculateFormworkArea,
operator.CalculateObjectVolumes,
operator.CalculateSingleQuantity,
operator.PerformQuantityTakeOff,
@@ -84,6 +84,21 @@ class CalculateObjectVolumes(bpy.types.Operator):
return {"FINISHED"}
class CalculateFormworkArea(bpy.types.Operator):
bl_idname = "bim.calculate_formwork_area"
bl_label = "Calculate Formwork Area"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects and context.active_object
def execute(self, context):
result = helper.calculate_formwork_area([o for o in context.selected_objects if o.type == "MESH"], context)
context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
return {"FINISHED"}
class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_single_quantity"
bl_label = "Calculate Single Quantity"
@@ -87,14 +87,16 @@ class BIM_PT_qto_simple(bpy.types.Panel):
row = layout.row()
row.prop(props, "qto_result", text="Results")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_circle_radius")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_edge_lengths")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_face_areas")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.calculate_object_volumes")
row = layout.row()
row.operator("bim.calculate_formwork_area")
class BIM_PT_qto_cost(bpy.types.Panel):
@@ -41,20 +41,7 @@ quantitytypes_enum = {}
def setup_quantity_types_enum():
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H
resources = {
"IfcCrewResource": ("IfcQuantityTime",),
"IfcLaborResource": ("IfcQuantityTime",),
"IfcSubContractResource": ("IfcQuantityTime",),
"IfcConstructionEquipmentResource": ("IfcQuantityTime",),
"IfcConstructionMaterialResource": (
"IfcQuantityVolume",
"IfcQuantityArea",
"IfcQuantityLength",
"IfcQuantityWeight",
),
"IfcConstructionProductResource": ("IfcQuantityCount",),
}
resources = ifcopenshell.util.resource.RESOURCES_TO_QUANTITIES
for resource, quantities in resources.items():
quantitytypes_enum[resource] = [(q, q, "") for q in quantities]
@@ -22,31 +22,32 @@ from . import ui, prop, operator, workspace
classes = (
operator.AssignContainer,
operator.ChangeSpatialLevel,
operator.ContractContainer,
operator.CopyToContainer,
operator.DeleteContainer,
operator.DereferenceStructure,
operator.DisableEditingContainer,
operator.EditContainerAttributes,
operator.EnableEditingContainer,
operator.ExpandContainer,
operator.LoadContainerManager,
operator.ReferenceStructure,
operator.RemoveContainer,
operator.SelectContainer,
operator.SelectSimilarContainer,
operator.SelectProduct,
operator.LoadContainerManager,
operator.EditContainerAttributes,
operator.AddBuildingStorey,
operator.ContractContainer,
operator.ExpandContainer,
operator.DeleteContainer,
operator.SelectDecomposedElements,
operator.SelectProduct,
operator.SelectSimilarContainer,
prop.SpatialElement,
prop.Element,
prop.BIMSpatialProperties,
prop.BIMObjectSpatialProperties,
prop.BIMContainer,
prop.BIMSpatialManagerProperties,
prop.BIMProjectTreeProperties,
ui.BIM_PT_spatial,
ui.BIM_UL_containers,
ui.BIM_UL_containers_manager,
ui.BIM_PT_SpatialManager,
ui.BIM_UL_elements,
ui.BIM_PT_project_tree,
workspace.Hotkey,
)
@@ -56,7 +57,7 @@ def register():
bpy.utils.register_tool(workspace.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False)
bpy.types.Scene.BIMSpatialProperties = bpy.props.PointerProperty(type=prop.BIMSpatialProperties)
bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
bpy.types.Scene.BIMProjectTreeProperties = bpy.props.PointerProperty(type=prop.BIMProjectTreeProperties)
def unregister():
@@ -64,4 +65,4 @@ def unregister():
bpy.utils.unregister_tool(workspace.SpatialTool)
del bpy.types.Scene.BIMSpatialProperties
del bpy.types.Object.BIMObjectSpatialProperties
del bpy.types.Scene.BIMSpatialManagerProperties
del bpy.types.Scene.BIMProjectTreeProperties
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
def refresh():
SpatialData.is_loaded = False
ProjectTreeData.is_loaded = False
class SpatialData:
@@ -31,28 +32,28 @@ class SpatialData:
@classmethod
def load(cls):
cls.data = {
"parent_container_id": cls.parent_container_id(),
"is_directly_contained": cls.is_directly_contained(),
"label": cls.label(),
"references": cls.references(),
"containers": cls.containers(),
}
cls.is_loaded = True
cls.data["poll"] = cls.poll()
if cls.data["poll"]:
cls.data.update(
{
"parent_container_id": cls.parent_container_id(),
"is_directly_contained": cls.is_directly_contained(),
"label": cls.label(),
"references": cls.references(),
}
)
@classmethod
def containers(cls):
results = {}
if tool.Ifc.get_schema() == "IFC2X3":
spatial_elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
spatial_elements = tool.Ifc.get().by_type("IfcSpatialElement")
for container in spatial_elements:
results[container.id()] = {
"type": container.is_a(),
"id": container.id(),
}
return results
def poll(cls):
if not bpy.context.active_object:
return False
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element:
return False
if element.is_a("IfcElement") or element.is_a("IfcAnnotation") or element.is_a("IfcGrid"):
return True
return False
@classmethod
def parent_container_id(cls):
@@ -81,3 +82,29 @@ class SpatialData:
@classmethod
def is_directly_contained(cls):
return bool(getattr(tool.Ifc.get_entity(bpy.context.active_object), "ContainedInStructure", False))
class ProjectTreeData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {
"containers": cls.containers(),
}
@classmethod
def containers(cls):
results = {}
if tool.Ifc.get_schema() == "IFC2X3":
spatial_elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
spatial_elements = tool.Ifc.get().by_type("IfcSpatialElement")
for container in spatial_elements:
results[container.id()] = {
"type": container.is_a(),
"id": container.id(),
}
return results
@@ -227,26 +227,6 @@ class DeleteContainer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.delete_container(tool.Ifc, tool.Spatial, tool.Geometry, container=tool.Ifc.get().by_id(self.container))
class AddBuildingStorey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_building_storey"
bl_label = "Add Storey"
bl_options = {"REGISTER", "UNDO"}
part_class: bpy.props.StringProperty()
def _execute(self, context):
active_container = tool.Spatial.get_active_container()
obj = tool.Ifc.get_object(active_container)
blenderbim.core.aggregate.add_part_to_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
tool.Blender,
obj=obj,
part_class=self.part_class,
part_name="Unnamed",
)
core.load_container_manager(tool.Spatial)
class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_decomposed_elements"
@@ -36,27 +36,21 @@ import ifcopenshell
def update_elevation(self, context):
entity = tool.Ifc.get().by_id(self.active_container_id)
obj = tool.Ifc.get_object(entity)
if not obj:
return
obj.location.z = self.elevation
if ifc_definition_id := self.ifc_definition_id:
entity = tool.Ifc.get().by_id(ifc_definition_id)
obj = tool.Ifc.get_object(entity)
if not obj:
return
obj.location.z = self.elevation
def update_name(self, context):
if ifc_definition_id := self.ifc_definition_id:
tool.Spatial.edit_container_name(tool.Ifc.get().by_id(ifc_definition_id), self.name)
def update_active_container_index(self, context):
if self.active_container_index < 0:
return
self.active_container_id = self.containers[self.active_container_index].ifc_definition_id
self.container_name = self.containers[self.active_container_index].name
self.elevation = self.containers[self.active_container_index].elevation
def updateContainerName(self, context):
props = context.scene.BIMSpatialManagerProperties
if not props.is_container_update_enabled or self.name == "Unnamed":
return
tool.Spatial.edit_container_name(tool.Ifc.get().by_id(props.active_container_id), self.name)
props.container_name = self.name
tool.Spatial.load_contained_elements()
def update_relating_container_from_object(self, context):
@@ -104,20 +98,34 @@ class BIMObjectSpatialProperties(PropertyGroup):
class BIMContainer(PropertyGroup):
name: StringProperty(name="Name", update=updateContainerName)
elevation: FloatProperty(name="Elevation", subtype="DISTANCE")
name: StringProperty(name="Name", update=update_name)
ifc_class: StringProperty(name="IFC Class")
description: StringProperty(name="Description")
long_name: StringProperty(name="Long Name")
elevation: FloatProperty(name="Elevation", subtype="DISTANCE", update=update_elevation)
level_index: IntProperty(name="Level Index")
has_children: BoolProperty(name="Has Children")
is_expanded: BoolProperty(name="Is Expanded")
ifc_definition_id: IntProperty(name="IFC Definition ID")
class BIMSpatialManagerProperties(PropertyGroup):
class Element(PropertyGroup):
name: StringProperty(name="Name")
is_class: BoolProperty(name="Is Class", default=False)
is_type: BoolProperty(name="Is Type", default=False)
total: IntProperty(name="Total")
class BIMProjectTreeProperties(PropertyGroup):
containers: CollectionProperty(name="Containers", type=BIMContainer)
contracted_containers: StringProperty(name="Contracted containers", default="[]")
expanded_containers: StringProperty(name="Expanded containers", default="[]")
active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index)
active_container_id: IntProperty(name="Active Container Id")
container_name: StringProperty(name="Container Name")
elevation: FloatProperty(name="Elevation", update=update_elevation, subtype="DISTANCE")
is_container_update_enabled: BoolProperty(name="Is Container Update Enabled", default=True) # TODO:review
elements: CollectionProperty(name="Elements", type=Element)
active_element_index: IntProperty(name="Active Element Index")
total_elements: IntProperty(name="Total Elements")
@property
def active_container(self):
if self.containers and self.active_container_index < len(self.containers):
return self.containers[self.active_container_index]
@@ -17,8 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.spatial.data import SpatialData
from blenderbim.bim.module.spatial.data import SpatialData, ProjectTreeData
import blenderbim.tool as tool
@@ -32,14 +31,9 @@ class BIM_PT_spatial(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
return False
oprops = context.active_object.BIMObjectProperties
if not oprops.ifc_definition_id:
return False
if not IfcStore.get_element(oprops.ifc_definition_id):
return False
return True
if not SpatialData.is_loaded:
SpatialData.load()
return SpatialData.data["poll"]
def draw(self, context):
if not SpatialData.is_loaded:
@@ -97,34 +91,41 @@ class BIM_UL_containers(UIList):
)
class BIM_PT_SpatialManager(Panel):
bl_label = "Spatial Manager"
bl_idname = "BIM_PT_SpatialManager"
class BIM_PT_project_tree(Panel):
bl_label = "Project Tree"
bl_idname = "BIM_PT_project_tree"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_tab_project_setup"
bl_parent_id = "BIM_PT_tab_project_tree"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and tool.Ifc.schema().name() != "IFC2X3"
return tool.Ifc.get()
def draw(self, context):
if not SpatialData.is_loaded:
SpatialData.load()
self.props = context.scene.BIMSpatialManagerProperties
row = self.layout.row()
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="Load Spatial Structure")
if 0 <= self.props.active_container_index < len(self.props.containers):
ifc_definition_id = self.props.containers[self.props.active_container_index].ifc_definition_id
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="Select Children")
spatial_data = SpatialData.data["containers"].get(ifc_definition_id, None)
if spatial_data and spatial_data["type"] in ["IfcBuildingStorey", "IfcBuilding"]:
row.operator("bim.add_building_storey", icon="ADD", text="Add storey").part_class = "IfcBuildingStorey"
row.operator("bim.delete_container", icon="X", text="Delete").container = ifc_definition_id
if not ProjectTreeData.is_loaded:
ProjectTreeData.load()
self.props = context.scene.BIMProjectTreeProperties
if self.props.active_container:
row = self.layout.row(align=True)
row.label(
text=f"Active {self.props.active_container.ifc_class}: {self.props.active_container.name}",
icon="OUTLINER_COLLECTION",
)
row.operator("bim.select_decomposed_elements", icon="HIDE_OFF", text="")
row.operator("bim.delete_container", icon="X", text="").container = (
self.props.active_container.ifc_definition_id
)
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="")
else:
row = self.layout.row(align=True)
row.label(text="Warning: No Active Container", icon="ERROR")
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="")
self.layout.template_list(
"BIM_UL_containers_manager",
"",
@@ -132,13 +133,33 @@ class BIM_PT_SpatialManager(Panel):
"containers",
self.props,
"active_container_index",
rows=10,
)
if not self.props.active_container:
return
# spatial_data = ProjectTreeData.data["containers"].get(ifc_definition_id, None)
# if spatial_data and spatial_data["type"] in ["IfcBuildingStorey", "IfcBuilding"]:
# row.operator("bim.add_building_storey", icon="ADD", text="Add storey").part_class = "IfcBuildingStorey"
if not self.props.total_elements:
row = self.layout.row()
row.label(text="No Contained Elements", icon="FILE_3D")
return
row = self.layout.row(align=True)
row.label(text=f"{self.props.total_elements} Contained Elements", icon="FILE_3D")
row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
self.layout.template_list(
"BIM_UL_elements",
"",
self.props,
"elements",
self.props,
"active_element_index",
)
row = self.layout.row()
if 0 <= self.props.active_container_index < len(self.props.containers):
row.prop(self.props, "container_name", text="")
row.prop(self.props, "elevation", text="")
op = row.operator("bim.edit_container_attributes", icon="CHECKMARK", text="Apply")
op.container = self.props.containers[self.props.active_container_index].ifc_definition_id
class BIM_UL_containers_manager(UIList):
@@ -146,22 +167,52 @@ class BIM_UL_containers_manager(UIList):
if item:
row = layout.row(align=True)
self.draw_hierarchy(row, item)
split1 = row.split(factor=0.7)
split1.prop(item, "name", emboss=False, text="")
split2 = row.split(factor=1)
split2.label(icon="BLANK1", text=tool.Unit.blender_format_unit(item.elevation))
row.prop(item, "name", emboss=False, text="")
row.prop(item, "long_name", emboss=False, text="")
col = row.column()
col.alignment = "RIGHT"
col.prop(item, "elevation", emboss=False, text="")
def draw_hierarchy(self, row, item):
for i in range(0, item.level_index):
row.label(text="", icon="BLANK1")
if item.has_children:
if item.is_expanded:
row.operator(
"bim.contract_container", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).container = item.ifc_definition_id
row.operator("bim.contract_container", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").container = (
item.ifc_definition_id
)
else:
row.operator(
"bim.expand_container", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).container = item.ifc_definition_id
row.operator("bim.expand_container", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").container = (
item.ifc_definition_id
)
else:
row.label(text="", icon="DOT")
row.label(text="", icon="BLANK1")
if item.ifc_class == "IfcSite":
row.label(text="", icon="WORLD")
elif item.ifc_class == "IfcBuilding":
row.label(text="", icon="HOME")
elif item.ifc_class == "IfcBuildingStorey":
row.label(text="", icon="LINENUMBERS_OFF")
elif item.ifc_class == "IfcSpace":
row.label(text="", icon="ANTIALIASED")
else:
row.label(text="", icon="META_PLANE")
class BIM_UL_elements(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
if item.is_class:
row.label(text="", icon="DISCLOSURE_TRI_DOWN")
row.label(text=item.name)
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
elif item.is_type:
row.label(text="", icon="BLANK1")
row.label(text="", icon="DISCLOSURE_TRI_DOWN")
row.label(text=item.name)
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
@@ -64,16 +64,10 @@ def add_layout_hotkey(layout, text, hotkey, description):
tool.Blender.add_layout_hotkey_operator(*args)
# NOTES before adding new operators:
# - add scene.BIMSpatialProperties
# - add SpatialData
class SpatialToolUI:
@classmethod
def draw(cls, context, layout):
cls.layout = layout
# cls.props = context.scene.BIMSpatialProperties
cls.model_props = context.scene.BIMModelProperties
row = cls.layout.row(align=True)
@@ -151,12 +145,10 @@ class Hotkey(bpy.types.Operator, Operator):
return operator.description or ""
def _execute(self, context):
# self.props = context.scene.BIMSpatialProperties
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
# self.props = context.scene.BIMSpatialProperties
return self.execute(context)
def draw(self, context):
@@ -24,6 +24,7 @@ classes = (
operator.AddPresentationStyle,
operator.AddStyle,
operator.AddSurfaceTexture,
operator.AssignStyleToSelected,
operator.BrowseExternalStyle,
operator.RemoveTextureMap,
operator.ChooseTextureMapPath,
@@ -23,6 +23,7 @@ import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.style as core
import ifcopenshell.api
import ifcopenshell.api.style
import ifcopenshell.util.representation
from blenderbim.bim.module.style.prop import switch_shading
from pathlib import Path
@@ -161,13 +162,6 @@ class UpdateCurrentStyle(bpy.types.Operator):
update_all: bpy.props.BoolProperty(name="Update All", default=False, options={"SKIP_SAVE"})
style_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
if not context.selected_objects:
cls.poll_message_set("No objects selected")
return False
return True
def invoke(self, context, event):
# updating all styles on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
@@ -958,3 +952,49 @@ class SaveUVToStyle(bpy.types.Operator, tool.Ifc.Operator):
self.report({"INFO"}, f"UV saved to the style {style.Name}")
return {"FINISHED"}
class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_style_to_selected"
bl_label = "Assign Style To Selected"
bl_description = "Assign style to the selected objects' active representations"
bl_options = {"REGISTER", "UNDO"}
style_id: bpy.props.IntProperty(name="Style ID")
@classmethod
def poll(cls, context):
if not context.selected_objects:
cls.poll_message_set("No objects selected")
return False
return True
def _execute(self, context):
if self.style_id == 0:
self.report({"ERROR"}, "No style provided")
return {"CANCELLED"}
ifc_file = tool.Ifc.get()
style = ifc_file.by_id(self.style_id)
representations: dict[ifcopenshell.entity_instance, bpy.types.Object] = {}
for obj in context.selected_objects:
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
representation = tool.Geometry.resolve_mapped_representation(representation)
representations.setdefault(representation, obj)
if not representations:
self.report({"INFO"}, "No IFC objects with representations selected.")
return {"FINISHED"}
for representation in representations:
ifcopenshell.api.style.assign_representation_styles(
ifc_file,
shape_representation=representation,
styles=[style],
should_use_presentation_style_assignment=tool.Geometry.should_use_presentation_style_assignment(),
)
tool.Geometry.reload_representation(representations.values())
return {"FINISHED"}
@@ -65,6 +65,8 @@ class BIM_PT_styles(Panel):
row.operator("bim.duplicate_style", text="", icon="DUPLICATE").style = style.ifc_definition_id
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
op = row.operator("bim.assign_style_to_selected", text="", icon="BRUSH_DATA")
op.style_id = style.ifc_definition_id
op = row.operator("bim.unlink_style", text="", icon="UNLINKED")
op.style = style.ifc_definition_id
op = row.operator("bim.enable_editing_style", text="", icon="GREASEPENCIL")
+19 -6
View File
@@ -202,9 +202,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
default=True,
description="If disabled, the toolbar will only load when an IFC model is active",
)
should_play_chaching_sound: BoolProperty(
name="Play A Cha-Ching Sound When Project Costs Updates", default=False
)
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
lock_grids_on_import: BoolProperty(name="Lock Grids By Default", default=True)
spatial_elements_unselectable: BoolProperty(name="Make Spatial Elements Unselectable By Default", default=True)
decorations_colour: bpy.props.FloatVectorProperty(
@@ -407,7 +405,7 @@ class BIM_PT_tabs(Panel):
if blenderbim.last_error:
box = self.layout.box()
box.alert=True
box.alert = True
row = box.row(align=True)
row.label(text="BlenderBIM experienced an error :(", icon="ERROR")
row.operator("bim.close_error", text="", icon="CANCEL")
@@ -442,6 +440,20 @@ class BIM_PT_tab_project_info(Panel):
pass
class BIM_PT_tab_project_tree(Panel):
bl_label = "Project Tree"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "PROJECT")
def draw(self, context):
pass
class BIM_PT_tab_project_setup(Panel):
bl_label = "Project Setup"
bl_space_type = "PROPERTIES"
@@ -490,7 +502,7 @@ class BIM_PT_tab_grouping_and_filtering(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED","HEADER_LAYOUT_EXPAND"}
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
@classmethod
def poll(cls, context):
@@ -503,8 +515,9 @@ class BIM_PT_tab_grouping_and_filtering(Panel):
# Draws help button on the right
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
row.operator("bim.open_uri", text="", icon="HELP").uri = \
row.operator("bim.open_uri", text="", icon="HELP").uri = (
"https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html"
)
class BIM_PT_tab_geometry(Panel):
+13 -2
View File
@@ -41,9 +41,12 @@ def assign_object(
related_obj: Optional[bpy.types.Object] = None,
) -> Union[ifcopenshell.entity_instance, None]:
if not aggregator.can_aggregate(relating_obj, related_obj):
return
raise IncompatibleAggregateError
relating_object = ifc.get_entity(relating_obj)
if aggregator.has_physical_body_representation(relating_object):
raise AggregateRepresentationError
rel = ifc.run(
"aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=ifc.get_entity(relating_obj)
"aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=relating_object
)
collector.assign(relating_obj)
collector.assign(related_obj)
@@ -84,3 +87,11 @@ def add_part_to_object(
part_obj = blender.create_ifc_object(ifc_class=part_class, name=part_name)
assign_object(ifc, aggregator, collector, relating_obj=obj, related_obj=part_obj)
blender.set_active_object(obj)
class IncompatibleAggregateError(Exception):
pass
class AggregateRepresentationError(Exception):
pass
@@ -484,12 +484,4 @@ def activate_drawing_view(
drawing_tool.import_annotations_in_group(drawing_tool.get_drawing_group(drawing))
blender.activate_camera(camera)
drawing_tool.isolate_camera_collection(camera)
try:
blender.set_active_object(camera)
except:
raise CameraNotAvailableError()
drawing_tool.activate_drawing(camera)
class CameraNotAvailableError(Exception):
pass
+1 -1
View File
@@ -831,7 +831,7 @@ class Spatial:
def can_reference(cls, structure, element): pass
def contract_container(cls, container): pass
def copy_xy(cls, src_obj, destination_obj): pass
def create_new_storey_li(cls, element, level_index): pass
def import_spatial_structure(cls, element, level_index): pass
def deselect_objects(cls): pass
def disable_editing(cls, obj): pass
def duplicate_object_and_data(cls, obj): pass
+10 -3
View File
@@ -30,9 +30,9 @@ class Aggregate(blenderbim.core.tool.Aggregate):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a("IfcElement"):
if relating_obj.data: # See #3973
return False
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a(
"IfcElement"
):
return True
if tool.Ifc.get_schema() == "IFC2X3":
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
@@ -46,6 +46,13 @@ class Aggregate(blenderbim.core.tool.Aggregate):
return True
return False
@classmethod
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
if element.is_a("IfcElement") or element.is_a("IfcElementType"): # See 3973
if ifcopenshell.util.representation.get_representation(element, "Model", "Body"):
return True
return False
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
obj.BIMObjectAggregateProperties.is_editing = False
+6 -3
View File
@@ -95,9 +95,12 @@ class Collector(blenderbim.core.tool.Collector):
# NOTE: won't allow assigning IfcElements to the IfcProject directly
# and some elements might get missing in other viewers if they're don't support displaying
# elements without hierarchy
blenderbim.core.aggregate.assign_object(
tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj
)
try:
blenderbim.core.aggregate.assign_object(
tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj
)
except blenderbim.core.aggregate.IncompatibleAggregateError:
pass
@classmethod
def assign(cls, obj: bpy.types.Object) -> None:
@@ -1752,6 +1752,8 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def activate_drawing(cls, camera: bpy.types.Object) -> None:
selected_objects_before = bpy.context.selected_objects
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing = tool.Ifc.get_entity(camera)
@@ -1839,6 +1841,10 @@ class Drawing(blenderbim.core.tool.Drawing):
cls.import_camera_props(drawing, camera)
for obj in selected_objects_before:
obj.hide_set(False)
obj.select_set(True)
@classmethod
def get_elements_in_camera_view(
cls, camera: bpy.types.Object, objs: list[ifcopenshell.entity_instance]
+17 -8
View File
@@ -725,11 +725,11 @@ class Geometry(blenderbim.core.tool.Geometry):
return False
@classmethod
def should_use_presentation_style_assignment(cls):
def should_use_presentation_style_assignment(cls) -> bool:
return bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment
@classmethod
def get_model_representations(cls):
def get_model_representations(cls) -> list[ifcopenshell.entity_instance]:
return tool.Ifc.get().by_type("IfcShapeRepresentation")
@classmethod
@@ -761,16 +761,25 @@ class Geometry(blenderbim.core.tool.Geometry):
Ensures that same representations won't be reloaded multiple times.
"""
objs = obj_or_objs if isinstance(obj_or_objs, Iterable) else [obj_or_objs]
ifc_file = tool.Ifc.get()
# Find all objects that use the same representation
# as there are possibility that some of them have openings
# (each representation with opening has a unique Mesh)
# and therefore reloading Mesh of it's type or occurrence
# might not be enough.
elements = set()
for obj in objs:
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
representation = tool.Geometry.resolve_mapped_representation(representation)
elements.update(ifcopenshell.util.element.get_elements_by_representation(ifc_file, representation))
# Filter out unique meshes to avoid
# reloading the same representation multiple times.
meshes_to_objects: dict[bpy.types.Mesh, bpy.types.Object]
meshes_to_objects = dict()
for obj in objs:
mesh = obj.data
if not mesh:
continue
meshes_to_objects.setdefault(mesh, obj)
meshes_to_objects = {(obj:=tool.Ifc.get_object(element)).data: obj for element in elements}
for obj in meshes_to_objects.values():
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+65 -31
View File
@@ -207,45 +207,79 @@ class Spatial(blenderbim.core.tool.Spatial):
src_obj.location = (destination_obj.location[0], destination_obj.location[1], z)
@classmethod
def load_container_manager(cls):
cls.props = bpy.context.scene.BIMSpatialManagerProperties
previous_container_index = cls.props.active_container_index
cls.props.containers.clear()
cls.contracted_containers = json.loads(cls.props.contracted_containers)
cls.props.is_container_update_enabled = False
parent = tool.Ifc.get().by_type("IfcProject")[0]
def load_contained_elements(cls):
props = bpy.context.scene.BIMProjectTreeProperties
props.elements.clear()
if not (container := props.active_container):
return
for object in ifcopenshell.util.element.get_parts(parent) or []:
if object.is_a("IfcSpatialElement") or object.is_a("IfcSpatialStructureElement"):
cls.create_new_storey_li(object, 0)
cls.props.is_container_update_enabled = True
# triggers spatial manager props setup
cls.props.active_container_index = min(previous_container_index, len(cls.props.containers) - 1)
container = tool.Ifc.get().by_id(container.ifc_definition_id)
results = {}
for element in ifcopenshell.util.element.get_contained(container):
element_type = ifcopenshell.util.element.get_type(element)
ifc_class = element.is_a()
type_name = element_type.Name or "Unnamed" if element_type else "Untyped"
results.setdefault(ifc_class, {}).setdefault(type_name, 0)
results[ifc_class][type_name] += 1
total_elements = 0
for ifc_class in sorted(results.keys()):
new = props.elements.add()
new.name = ifc_class
new.is_class = True
total = 0
for type_name in sorted(results[ifc_class].keys()):
new2 = props.elements.add()
new2.name = type_name
new2.is_type = True
new2.total = results[ifc_class][type_name]
total += new2.total
new.total = total
total_elements += total
props.total_elements = total_elements
@classmethod
def create_new_storey_li(cls, element, level_index):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new = cls.props.containers.add()
new.name = element.Name or "Unnamed"
new.long_name = element.LongName or ""
new.has_decomposition = bool(element.IsDecomposedBy)
new.ifc_definition_id = element.id()
new.elevation = ifcopenshell.util.placement.get_storey_elevation(element) * si_conversion
def load_container_manager(cls):
props = bpy.context.scene.BIMProjectTreeProperties
previous_container_index = props.active_container_index
props.containers.clear()
cls.contracted_containers = json.loads(props.contracted_containers)
props.is_container_update_enabled = False
parent = tool.Ifc.get().by_type("IfcProject")[0]
for subelement in ifcopenshell.util.element.get_parts(parent) or []:
if subelement.is_a("IfcSpatialElement") or subelement.is_a("IfcSpatialStructureElement"):
cls.import_spatial_structure(subelement, 0)
props.is_container_update_enabled = True
# triggers spatial manager props setup
props.active_container_index = min(previous_container_index, len(props.containers) - 1)
@classmethod
def import_spatial_structure(cls, element, level_index):
props = bpy.context.scene.BIMProjectTreeProperties
new = props.containers.add()
new.ifc_class = element.is_a()
new.name = element.Name or "Unnamed"
new.description = element.Description or ""
new.long_name = element.LongName or ""
new.elevation = ifcopenshell.util.placement.get_storey_elevation(element)
new.is_expanded = element.id() not in cls.contracted_containers
new.level_index = level_index
if new.has_decomposition:
new.has_children = True
if new.is_expanded:
for related_object in ifcopenshell.util.element.get_parts(element) or []:
if related_object.is_a("IfcSpatialElement") or related_object.is_a("IfcSpatialStructureElement"):
cls.create_new_storey_li(related_object, level_index + 1)
children = ifcopenshell.util.element.get_parts(element)
new.has_children = bool(children)
new.ifc_definition_id = element.id()
if new.is_expanded:
for child in children or []:
cls.import_spatial_structure(child, level_index + 1)
@classmethod
def edit_container_attributes(cls, entity):
# TODO
obj = tool.Ifc.get_object(entity)
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
name = bpy.context.scene.BIMSpatialManagerProperties.container_name
name = bpy.context.scene.BIMProjectTreeProperties.container_name
if name != entity.Name:
cls.edit_container_name(entity, name)
@@ -255,21 +289,21 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod
def get_active_container(cls):
props = bpy.context.scene.BIMSpatialManagerProperties
props = bpy.context.scene.BIMProjectTreeProperties
if props.active_container_index < len(props.containers):
container = tool.Ifc.get().by_id(props.containers[props.active_container_index].ifc_definition_id)
return container
@classmethod
def contract_container(cls, container):
props = bpy.context.scene.BIMSpatialManagerProperties
props = bpy.context.scene.BIMProjectTreeProperties
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.append(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def expand_container(cls, container):
props = bpy.context.scene.BIMSpatialManagerProperties
props = bpy.context.scene.BIMProjectTreeProperties
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.remove(container.id())
props.contracted_containers = json.dumps(contracted_containers)
+1 -1
View File
@@ -36,7 +36,7 @@ Some of these add-ons are not shipped with Blender:
import GIS data, grab elevation data from the web, and generate TINs from
survey points and contours.
- `Ladybug Tools for Blender
<https://github.com/Andrej730/ladybug-blender/releases/download/ladybug-blender-240419/ladybug-blender-240419.zip>`__ - Ladybug Tools
<https://github.com/ladybug-tools/ladybug-blender/releases/download/ladybug-blender-240529/ladybug-blender-240529.zip>`__ - Ladybug Tools
is an extension of Sverchok for environmental analysis and building physics
simulation. It allows analysis of solar, daylight, energy, and CFD.
- `Topologic <https://topologic.app/>`__ - Perform spatial and topological
+19 -8
View File
@@ -18,6 +18,9 @@
import bpy
import ifcopenshell
import ifcopenshell.api.root
import ifcopenshell.api.unit
import ifcopenshell.api.context
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
@@ -92,16 +95,24 @@ class TestCanAggregate(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_aggregate(element_obj, subelement_obj) is False
def test_aggregates_with_meshes_are_invalid(self):
class TestHasPhysicalBodyRepresentation(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcElementAssembly()
element_obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
tool.Ifc.link(element, element_obj)
subelement = ifc.createIfcBeam()
subelement_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(subelement, subelement_obj)
assert subject.can_aggregate(element_obj, subelement_obj) is False
assert subject.has_physical_body_representation(element) is False
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
ifcopenshell.api.unit.assign_unit(ifc)
context = ifcopenshell.api.context.add_context(ifc, context_type="Model")
body = ifcopenshell.api.context.add_context(
ifc, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=context
)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc)
origin = builder.create_axis2_placement_3d()
block = ifc.createIfcCsgSolid(ifc.createIfcBlock(origin, 200, 200, 200))
rep = builder.get_representation(context=body, items=[block])
ifcopenshell.api.geometry.assign_representation(ifc, product=element, representation=rep)
assert subject.has_physical_body_representation(element) is True
class TestDisableEditing(NewFile):
+30 -119
View File
@@ -24,8 +24,8 @@ import test.bim.bootstrap
import blenderbim.core.tool
import blenderbim.core.root
import blenderbim.tool as tool
import blenderbim.bim.module.qto.calculator as calculator
from blenderbim.tool.qto import Qto as subject
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
class TestImplementsTool(test.bim.bootstrap.NewFile):
@@ -45,48 +45,6 @@ class TestSetQtoResult(test.bim.bootstrap.NewFile):
assert bpy.context.scene.BIMQtoProperties.qto_result == "123.457"
class TestGetApplicableQuantityNames(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
schema = ifc.schema
properties_templates = (
ifcopenshell.util.pset.PsetQto(schema)
.get_by_name("Qto_WallBaseQuantities")
.get_info()["HasPropertyTemplates"]
)
applicable_quantity_names = [a.Name for a in properties_templates]
assert subject.get_applicable_quantity_names("Qto_WallBaseQuantities") == applicable_quantity_names
class TestGetApplicableBaseQuantityName(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
assert subject.get_applicable_base_quantity_name(wall) == "Qto_WallBaseQuantities"
def test_no_quantities(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
product = ifc.by_type("IfcProject")[0]
assert subject.get_applicable_base_quantity_name(product) == None
def test_anomaly_named_quantities(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingElementProxy")
# Prioritized over Qto_BodyGeometryValidation.
assert subject.get_applicable_base_quantity_name(product) == "Qto_BuildingElementProxyQuantities"
def test_prioritize_base_over_other_qto(self):
ifc = ifcopenshell.file(schema="IFC4X3")
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
assert subject.get_applicable_base_quantity_name(product) == "Qto_WallBaseQuantities"
class TestGetRoundedValue(test.bim.bootstrap.NewFile):
def test_run(self):
quantity = 1.2345
@@ -97,12 +55,14 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile):
def setup_file(self):
self.ifc = ifcopenshell.file()
tool.Ifc.set(self.ifc)
project = ifcopenshell.api.run("root.create_entity", self.ifc, ifc_class="IfcProject", name="My Project")
ifcopenshell.api.run("root.create_entity", self.ifc, ifc_class="IfcProject", name="My Project")
def setup_units(self, units):
ifcopenshell.api.run("unit.assign_unit", self.ifc, **units)
def calculate_quantities(self, obj):
import ifc5d.qto
context = ifcopenshell.api.run("context.add_context", self.ifc, context_type="Model")
bpy.ops.mesh.primitive_cube_add(location=(0.0, 0.0, 0.0), size=2)
obj = bpy.context.active_object
@@ -115,12 +75,32 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile):
predefined_type="ELEMENTEDWALL",
context=context,
)
calculator = QtoCalculator()
base_qto = ifcopenshell.api.run("pset.add_qto", self.ifc, product=element, name="Qto_WallBaseQuantities")
quantities = subject.get_calculated_object_quantities(
calculator=calculator, qto_name="Qto_WallBaseQuantities", obj=obj
)
return quantities
rules = {
"calculators": {
"Blender": {
"IfcWall": {
"Qto_WallBaseQuantities": {
"GrossFootprintArea": "get_gross_footprint_area",
"GrossSideArea": "get_gross_side_area",
"GrossVolume": "get_gross_volume",
"GrossWeight": "get_gross_weight",
"Height": "get_height",
"Length": "get_length",
"NetFootprintArea": "get_net_footprint_area",
"NetSideArea": "get_net_side_area",
"NetVolume": "get_net_volume",
"NetWeight": "get_net_weight",
"Width": "get_width",
}
},
}
}
}
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, {element}, rules)
return {k: round(v, 3) for k, v in results[element]["Qto_WallBaseQuantities"].items() if v is not None}
def test_meters_project_unit(self):
self.setup_file()
@@ -186,35 +166,6 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile):
assert quantities["NetVolume"] == 282.517
class TestAddObjectBaseQto(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
project = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject", name="My Project")
context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
bpy.ops.mesh.primitive_cube_add(location=(0.0, 0.0, 0.0), size=2)
obj = bpy.context.active_object
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcWall",
predefined_type="ELEMENTEDWALL",
context=context,
)
assert subject.add_object_base_qto(obj).Name == "Qto_WallBaseQuantities"
class TestAddProductBaseQto(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
base_qto = subject.add_product_base_qto(wall)
assert base_qto.Name == "Qto_WallBaseQuantities"
class TestGetBaseQto(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
@@ -236,46 +187,6 @@ class TestGetBaseQto(test.bim.bootstrap.NewFile):
product = tool.Ifc.get_entity(wall_obj)
assert not subject.get_base_qto(product) == True
def test_anomaly_named_quantities(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingElementProxy")
tool.Ifc.run(
"pset.add_qto",
product=product,
name="EQto_BodyGeometryValidation",
)
tool.Ifc.run(
"pset.add_qto",
product=product,
name="Qto_BuildingElementProxyQuantities",
)
# Prioritized over Qto_BodyGeometryValidation.
base_qto_name = subject.get_base_qto(product).Name
assert base_qto_name == "Qto_BuildingElementProxyQuantities"
# Ensure methods are in sync.
assert base_qto_name == subject.get_applicable_base_quantity_name(product)
def test_prioritize_base_over_other_qto(self):
ifc = ifcopenshell.file(schema="IFC4X3")
tool.Ifc.set(ifc)
product = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
tool.Ifc.run(
"pset.add_qto",
product=product,
name="Qto_BodyGeometryValidation",
)
tool.Ifc.run(
"pset.add_qto",
product=product,
name="Qto_WallBaseQuantities",
)
# Prioritized over Qto_BodyGeometryValidation.
base_qto_name = subject.get_base_qto(product).Name
assert base_qto_name == "Qto_WallBaseQuantities"
# Ensure methods are in sync.
assert base_qto_name == subject.get_applicable_base_quantity_name(product)
class TestGetRelatedCostItemQuantities(test.bim.bootstrap.NewFile):
def test_run(self):
+1 -1
View File
@@ -76,7 +76,7 @@ class SI2ProjectUnitConverter:
"IfcLengthMeasure": "METRE",
"IfcMassMeasure": "GRAM",
"IfcTimeMeasure": "SECOND",
"IfcVolumeMeasure": "CUBIE_METRE",
"IfcVolumeMeasure": "CUBIC_METRE",
}
def convert(self, value, measure):
@@ -102,6 +102,7 @@ elements in your filter group based on their criteria.
"Material", "Filter", "``material{{=}}{{value}}``", "``material=Foo`` specifies the criteria that elements must have a IfcMaterial assigned directly or indirectly (such as within a layer set). That IfcMaterial must have either a ``Name`` or ``Category`` attribute with a value of ``Foo``."
"Classification", "Filter", "``classification{{=}}{{value}}``", "``classification=Foo`` specifies the criteria that elements must have an IfcClassificationReference with an ``Identification`` attribute with a value of ``Foo``."
"Location", "Filter", "``location{{=}}{{value}}``", "``location=Foo`` specifies the criteria that elements must be contained directly or indirectly in a spatial element with a ``Name`` attribute with a value of ``Foo``."
"Parent", "Filter", "``parent{{=}}{{value}}``", "``parent=Foo`` specifies the criteria that elements must be a direct or indirect child in the spatial hierarchy to an element with a ``Name`` attribute with a value of ``Foo``."
"Query", "Filter", "``query:{{keys}}{{=}}{{value}}``", "``query:types.count=0`` specifies the criteria that elements must have zero type occurrences. The query keys corresponds to the syntax used in the `Getting element values`_ section"
When you specify a filter with a ``{{=}}`` check, you can choose from one of
@@ -183,6 +184,7 @@ Valid keys are:
"``storey``", "Gets the first IfcBuildingStorey spatial element that an element is contained in."
"``building``", "Gets the first IfcBuilding spatial element that an element is contained in."
"``site``", "Gets the first IccSite spatial element that an element is contained in."
"``parent``", "Gets the parent element in the spatial hierarchy."
"``material`` or ``mat``", "Gets the assigned material, which may be a material set."
"``item`` or ``i``", "If the previous key returns a material set, gets the relevant material set items"
"``materials`` or ``mats``", "Gets a list of IfcMaterials assigned directly or indirectly (such as via a material set) to the element"
@@ -28,3 +28,8 @@ from .assign_object import assign_object
from .unassign_object import unassign_object
wrap_usecases(__path__, __name__)
__all__ = [
"assign_object",
"unassign_object",
]
@@ -27,3 +27,7 @@ from .. import wrap_usecases
from .edit_attributes import edit_attributes
wrap_usecases(__path__, __name__)
__all__ = [
"edit_attributes",
]
@@ -30,3 +30,10 @@ from .edit_attributes import edit_attributes
from .remove_boundary import remove_boundary
wrap_usecases(__path__, __name__)
__all__ = [
"assign_connection_geometry",
"copy_boundary",
"edit_attributes",
"remove_boundary",
]
@@ -36,3 +36,12 @@ from .remove_classification import remove_classification
from .remove_reference import remove_reference
wrap_usecases(__path__, __name__)
__all__ = [
"add_classification",
"add_reference",
"edit_classification",
"edit_reference",
"remove_classification",
"remove_reference",
]
@@ -34,3 +34,15 @@ from .remove_metric import remove_metric
from .unassign_constraint import unassign_constraint
wrap_usecases(__path__, __name__)
__all__ = [
"add_metric",
"add_metric_reference",
"add_objective",
"assign_constraint",
"edit_metric",
"edit_objective",
"remove_constraint",
"remove_metric",
"unassign_constraint",
]
@@ -31,3 +31,9 @@ from .edit_context import edit_context
from .remove_context import remove_context
wrap_usecases(__path__, __name__)
__all__ = [
"add_context",
"edit_context",
"remove_context",
]
@@ -27,3 +27,8 @@ from .assign_control import assign_control
from .unassign_control import unassign_control
wrap_usecases(__path__, __name__)
__all__ = [
"assign_control",
"unassign_control",
]
@@ -46,3 +46,25 @@ from .remove_cost_value import remove_cost_value
from .unassign_cost_item_quantity import unassign_cost_item_quantity
wrap_usecases(__path__, __name__)
__all__ = [
"add_cost_item",
"add_cost_item_quantity",
"add_cost_schedule",
"add_cost_value",
"assign_cost_item_quantity",
"assign_cost_value",
"calculate_cost_item_resource_value",
"copy_cost_item",
"copy_cost_item_values",
"edit_cost_item",
"edit_cost_item_quantity",
"edit_cost_schedule",
"edit_cost_value",
"edit_cost_value_formula",
"remove_cost_item",
"remove_cost_item_quantity",
"remove_cost_schedule",
"remove_cost_value",
"unassign_cost_item_quantity",
]
@@ -35,3 +35,14 @@ from .remove_reference import remove_reference
from .unassign_document import unassign_document
wrap_usecases(__path__, __name__)
__all__ = [
"add_information",
"add_reference",
"assign_document",
"edit_information",
"edit_reference",
"remove_information",
"remove_reference",
"unassign_document",
]
@@ -28,3 +28,9 @@ from .edit_text_literal import edit_text_literal
from .unassign_product import unassign_product
wrap_usecases(__path__, __name__)
__all__ = [
"assign_product",
"edit_text_literal",
"unassign_product",
]
@@ -61,3 +61,28 @@ from .remove_representation import remove_representation
from .unassign_representation import unassign_representation
wrap_usecases(__path__, __name__)
__all__ = [
"add_axis_representation",
"add_boolean",
"add_door_representation",
"add_footprint_representation",
"add_mesh_representation",
"add_profile_representation",
"add_railing_representation",
"add_representation",
"add_slab_representation",
"add_wall_representation",
"add_window_representation",
"assign_representation",
"connect_element",
"connect_path",
"create_2pt_wall",
"disconnect_element",
"disconnect_path",
"edit_object_placement",
"map_representation",
"remove_boolean",
"remove_representation",
"unassign_representation",
]
@@ -29,3 +29,9 @@ from .edit_georeferencing import edit_georeferencing
from .remove_georeferencing import remove_georeferencing
wrap_usecases(__path__, __name__)
__all__ = [
"add_georeferencing",
"edit_georeferencing",
"remove_georeferencing",
]
@@ -30,3 +30,9 @@ from .create_grid_axis import create_grid_axis
from .remove_grid_axis import remove_grid_axis
wrap_usecases(__path__, __name__)
__all__ = [
"create_axis_curve",
"create_grid_axis",
"remove_grid_axis",
]
@@ -32,3 +32,12 @@ from .unassign_group import unassign_group
from .update_group_products import update_group_products
wrap_usecases(__path__, __name__)
__all__ = [
"add_group",
"assign_group",
"edit_group",
"remove_group",
"unassign_group",
"update_group_products",
]
@@ -33,3 +33,11 @@ from .remove_layer import remove_layer
from .unassign_layer import unassign_layer
wrap_usecases(__path__, __name__)
__all__ = [
"add_layer",
"assign_layer",
"edit_layer",
"remove_layer",
"unassign_layer",
]
@@ -34,3 +34,14 @@ from .remove_reference import remove_reference
from .unassign_reference import unassign_reference
wrap_usecases(__path__, __name__)
__all__ = [
"add_library",
"add_reference",
"assign_reference",
"edit_library",
"edit_reference",
"remove_library",
"remove_reference",
"unassign_reference",
]
@@ -58,3 +58,30 @@ from .reorder_set_item import reorder_set_item
from .unassign_material import unassign_material
wrap_usecases(__path__, __name__)
__all__ = [
"add_constituent",
"add_layer",
"add_list_item",
"add_material",
"add_material_set",
"add_profile",
"assign_material",
"assign_profile",
"copy_material",
"edit_assigned_material",
"edit_constituent",
"edit_layer",
"edit_layer_usage",
"edit_material",
"edit_profile",
"edit_profile_usage",
"remove_constituent",
"remove_layer",
"remove_list_item",
"remove_material",
"remove_material_set",
"remove_profile",
"reorder_set_item",
"unassign_material",
]
@@ -34,3 +34,10 @@ from .reorder_nesting import reorder_nesting
from .unassign_object import unassign_object
wrap_usecases(__path__, __name__)
__all__ = [
"assign_object",
"change_nest",
"reorder_nesting",
"unassign_object",
]
@@ -50,3 +50,29 @@ from .unassign_actor import unassign_actor
from .update_owner_history import update_owner_history
wrap_usecases(__path__, __name__)
__all__ = [
"add_actor",
"add_address",
"add_application",
"add_organisation",
"add_person",
"add_person_and_organisation",
"add_role",
"assign_actor",
"create_owner_history",
"edit_actor",
"edit_address",
"edit_organisation",
"edit_person",
"edit_role",
"remove_actor",
"remove_address",
"remove_application",
"remove_organisation",
"remove_person",
"remove_person_and_organisation",
"remove_role",
"unassign_actor",
"update_owner_history",
]
@@ -30,3 +30,11 @@ from .edit_profile import edit_profile
from .remove_profile import remove_profile
wrap_usecases(__path__, __name__)
__all__ = [
"add_arbitrary_profile",
"add_arbitrary_profile_with_voids",
"add_parameterized_profile",
"edit_profile",
"remove_profile",
]
@@ -33,3 +33,10 @@ from .create_file import create_file
from .unassign_declaration import unassign_declaration
wrap_usecases(__path__, __name__)
__all__ = [
"append_asset",
"assign_declaration",
"create_file",
"unassign_declaration",
]
@@ -31,3 +31,11 @@ from .edit_qto import edit_qto
from .remove_pset import remove_pset
wrap_usecases(__path__, __name__)
__all__ = [
"add_pset",
"add_qto",
"edit_pset",
"edit_qto",
"remove_pset",
]
@@ -33,3 +33,12 @@ from .remove_prop_template import remove_prop_template
from .remove_pset_template import remove_pset_template
wrap_usecases(__path__, __name__)
__all__ = [
"add_prop_template",
"add_pset_template",
"edit_prop_template",
"edit_pset_template",
"remove_prop_template",
"remove_pset_template",
]
@@ -38,3 +38,18 @@ from .remove_resource_quantity import remove_resource_quantity
from .unassign_resource import unassign_resource
wrap_usecases(__path__, __name__)
__all__ = [
"add_resource",
"add_resource_quantity",
"add_resource_time",
"assign_resource",
"calculate_resource_usage",
"calculate_resource_work",
"edit_resource",
"edit_resource_quantity",
"edit_resource_time",
"remove_resource",
"remove_resource_quantity",
"unassign_resource",
]
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.element
import ifcopenshell.util.resource
def add_resource_quantity(
@@ -66,6 +67,14 @@ def add_resource_quantity(
"""
settings = {"resource": resource, "ifc_class": ifc_class}
resource_type = resource.is_a()
supported_quantities = ifcopenshell.util.resource.RESOURCES_TO_QUANTITIES[resource_type]
if ifc_class not in supported_quantities:
raise ValueError(
f"Resource type '{resource_type}' does not support quantity type '{ifc_class}'. "
f"Supported quantities: {','.join(supported_quantities)}"
)
quantity = file.create_entity(settings["ifc_class"], Name="Unnamed")
# 3 IfcPhysicalSimpleQuantity Value
if settings["ifc_class"] == "IfcQuantityCount":
@@ -74,8 +74,9 @@ def remove_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_insta
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
if settings["resource"].Usage:
file.remove(settings["resource"].Usage)
# Usage was added in IFC4.
if usage := getattr(settings["resource"], "Usage", None):
file.remove(usage)
if settings["resource"].BaseQuantity:
ifcopenshell.api.run(
"resource.remove_resource_quantity",
@@ -33,3 +33,10 @@ from .reassign_class import reassign_class
from .remove_product import remove_product
wrap_usecases(__path__, __name__)
__all__ = [
"copy_class",
"create_entity",
"reassign_class",
"remove_product",
]
@@ -67,3 +67,44 @@ from .unassign_recurrence_pattern import unassign_recurrence_pattern
from .unassign_sequence import unassign_sequence
wrap_usecases(__path__, __name__)
__all__ = [
"add_task",
"add_task_time",
"add_time_period",
"add_work_calendar",
"add_work_plan",
"add_work_schedule",
"add_work_time",
"assign_lag_time",
"assign_process",
"assign_product",
"assign_recurrence_pattern",
"assign_sequence",
"assign_workplan",
"calculate_task_duration",
"cascade_schedule",
"create_baseline",
"duplicate_task",
"edit_lag_time",
"edit_recurrence_pattern",
"edit_sequence",
"edit_task",
"edit_task_time",
"edit_work_calendar",
"edit_work_plan",
"edit_work_schedule",
"edit_work_time",
"recalculate_schedule",
"remove_task",
"remove_time_period",
"remove_work_calendar",
"remove_work_plan",
"remove_work_schedule",
"remove_work_time",
"unassign_lag_time",
"unassign_process",
"unassign_product",
"unassign_recurrence_pattern",
"unassign_sequence",
]
@@ -29,3 +29,10 @@ from .reference_structure import reference_structure
from .unassign_container import unassign_container
wrap_usecases(__path__, __name__)
__all__ = [
"assign_container",
"dereference_structure",
"reference_structure",
"unassign_container",
]
@@ -46,3 +46,27 @@ from .remove_structural_load_group import remove_structural_load_group
from .unassign_structural_analysis_model import unassign_structural_analysis_model
wrap_usecases(__path__, __name__)
__all__ = [
"add_structural_activity",
"add_structural_analysis_model",
"add_structural_boundary_condition",
"add_structural_load",
"add_structural_load_case",
"add_structural_load_group",
"add_structural_member_connection",
"assign_structural_analysis_model",
"edit_structural_analysis_model",
"edit_structural_boundary_condition",
"edit_structural_connection_cs",
"edit_structural_item_axis",
"edit_structural_load",
"edit_structural_load_case",
"remove_structural_analysis_model",
"remove_structural_boundary_condition",
"remove_structural_connection_condition",
"remove_structural_load",
"remove_structural_load_case",
"remove_structural_load_group",
"unassign_structural_analysis_model",
]
@@ -38,3 +38,18 @@ from .unassign_material_style import unassign_material_style
from .unassign_representation_styles import unassign_representation_styles
wrap_usecases(__path__, __name__)
__all__ = [
"add_style",
"add_surface_style",
"add_surface_textures",
"assign_material_style",
"assign_representation_styles",
"edit_presentation_style",
"edit_surface_style",
"remove_style",
"remove_styled_representation",
"remove_surface_style",
"unassign_material_style",
"unassign_representation_styles",
]
@@ -39,3 +39,18 @@ from .unassign_port import unassign_port
from .unassign_system import unassign_system
wrap_usecases(__path__, __name__)
__all__ = [
"add_port",
"add_system",
"assign_flow_control",
"assign_port",
"assign_system",
"connect_port",
"disconnect_port",
"edit_system",
"remove_system",
"unassign_flow_control",
"unassign_port",
"unassign_system",
]
@@ -30,3 +30,9 @@ from .map_type_representations import map_type_representations
from .unassign_type import unassign_type
wrap_usecases(__path__, __name__)
__all__ = [
"assign_type",
"map_type_representations",
"unassign_type",
]
@@ -36,3 +36,16 @@ from .remove_unit import remove_unit
from .unassign_unit import unassign_unit
wrap_usecases(__path__, __name__)
__all__ = [
"add_context_dependent_unit",
"add_conversion_based_unit",
"add_monetary_unit",
"add_si_unit",
"assign_unit",
"edit_derived_unit",
"edit_monetary_unit",
"edit_named_unit",
"remove_unit",
"unassign_unit",
]
@@ -31,3 +31,10 @@ from .remove_filling import remove_filling
from .remove_opening import remove_opening
wrap_usecases(__path__, __name__)
__all__ = [
"add_filling",
"add_opening",
"remove_filling",
"remove_opening",
]
@@ -332,7 +332,7 @@ class entity_instance:
self.wrapped_data.setArgumentAsNull(idx)
except RuntimeError as e:
if e.args == ("Attribute not set",):
raise ValueError(
raise TypeError(
"attribute '%s' is not optional for entity instance of type '%s'"
% (self.wrapped_data.get_argument_name(idx), self.wrapped_data.is_a(True))
)
@@ -648,6 +648,8 @@ def get_styles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entit
return styles
# TODO: ifc_file argument is unnecessary for some methods now
# since we have entity_instance.file, so we can deprecate it.
def get_elements_by_material(
ifc_file: ifcopenshell.file, material: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
@@ -1041,7 +1043,9 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti
- Voiding: the opening voids another physical element, such as a hole in a wall
:param element: Any physical or spatial element in the tree
:type element: ifcopenshell.entity_instance
:return: Its parent. This must exist for any valid file, or None if we've reached the IfcProject.
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -1065,7 +1069,9 @@ def get_filled_void(element: ifcopenshell.entity_instance) -> Union[ifcopenshell
Examples include windows and doors which fill a opening inside a wall.
:param element: The building element, typically a window or door
:type element: ifcopenshell.entity_instance
:return: The IfcOpeningElement that it is filling
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -1084,7 +1090,9 @@ def get_voided_element(element: ifcopenshell.entity_instance) -> Union[ifcopensh
For all valid models, this should never return None.
:param element: The IfcOpeningElement
:type element: ifcopenshell.entity_instance
:return: The building element, such as a wall or slab
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -1102,7 +1110,9 @@ def get_aggregate(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.e
Retrieves the aggregate parent of an element.
:param element: The IFC element
:type element: ifcopenshell.entity_instance
:return: The aggregate of the element
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -1116,14 +1126,14 @@ def get_aggregate(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.e
return decomposes[0].RelatingObject
def get_nest(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance]:
def get_nest(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""
Retrieves the nest parent of an element.
:param element: The IFC element
:type element: ifcopenshell.entity_instance
:return: The nested whole of the element
:rtype: ifcopenshell.entity_instance
:rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -1159,6 +1169,28 @@ def get_parts(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity
if (is_decomposed_by := getattr(element, "IsDecomposedBy", None)) is not None and is_decomposed_by:
if is_decomposed_by[0].is_a("IfcRelAggregates"):
return is_decomposed_by[0].RelatedObjects
return []
def get_contained(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""
Retrieves the contained elements of spatial element.
:param element: The IFC element
:type element: ifcopenshell.entity_instance
:return: The parts of the element
:rtype: list[ifcopenshell.entity_instance]
Example:
.. code:: python
element = file.by_type("IfcBuildingStorey")[0]
elements = ifcopenshell.util.element.get_contained(element)
"""
if (rel := getattr(element, "ContainsElements", None)) is not None and rel:
return rel[0].RelatedElements
return []
def get_components(element: ifcopenshell.entity_instance, include_ports=False) -> list[ifcopenshell.entity_instance]:
@@ -1188,6 +1220,7 @@ def get_components(element: ifcopenshell.entity_instance, include_ports=False) -
elif (is_decomposed_by := getattr(element, "IsDecomposedBy", None)) is not None and is_decomposed_by:
if is_decomposed_by[0].is_a("IfcRelNests"):
return is_decomposed_by[0].RelatedObjects
return []
ReferenceData = namedtuple("ReferenceData", "inverse_attribute, rel_class, relating_element_attribute")
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.util.placement
from typing import Optional, Union, TypedDict
@@ -98,12 +99,12 @@ def resolve_representation(representation: ifcopenshell.entity_instance) -> ifco
class ResolvedItemDict(TypedDict):
matrix: np.array
matrix: npt.NDArray[np.float64]
item: ifcopenshell.entity_instance
def resolve_items(
representation: ifcopenshell.entity_instance, matrix: Optional[np.array] = None
representation: ifcopenshell.entity_instance, matrix: Optional[npt.NDArray[np.float64]] = None
) -> list[ResolvedItemDict]:
if matrix is None:
matrix = np.eye(4)
@@ -23,6 +23,20 @@ from typing import Union, Any
PRODUCTIVITY_PSET_DATA = Union[dict[str, Any], None]
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H
RESOURCES_TO_QUANTITIES: dict[str, tuple[str, ...]] = {
"IfcCrewResource": ("IfcQuantityTime",),
"IfcLaborResource": ("IfcQuantityTime",),
"IfcSubContractResource": ("IfcQuantityTime",),
"IfcConstructionEquipmentResource": ("IfcQuantityTime",),
"IfcConstructionMaterialResource": (
"IfcQuantityVolume",
"IfcQuantityArea",
"IfcQuantityLength",
"IfcQuantityWeight",
),
"IfcConstructionProductResource": ("IfcQuantityCount",),
}
def get_productivity(resource: ifcopenshell.entity_instance, should_inherit: bool = True) -> PRODUCTIVITY_PSET_DATA:
@@ -687,21 +687,45 @@ class FacetTransformer(lark.Transformer):
def parent(self, args):
comparison, value = args
def filter_function(element):
parents = []
result = False
if parent := ifcopenshell.util.element.get_parent(element):
parents.append(parent)
while parents:
parent = parents.pop()
if self.compare(parent.Name, comparison, value):
result = True
break
if grandparent := ifcopenshell.util.element.get_parent(parent):
parents.append(grandparent)
return result if comparison == "=" else not result
parents = set()
for rel in self.file.by_type("IfcRelAggregates"):
parent = rel.RelatingObject
if parent and self.compare(parent.Name, comparison, value):
parents.add(parent)
self.elements = set(filter(filter_function, self.elements))
for rel in self.file.by_type("IfcRelContainedInSpatialStructure"):
parent = rel.RelatingStructure
if parent and self.compare(parent.Name, comparison, value):
parents.add(parent)
for rel in self.file.by_type("IfcRelNests"):
parent = rel.RelatingObject
if parent and self.compare(parent.Name, comparison, value):
parents.add(parent)
for rel in self.file.by_type("IfcRelVoidsElement"):
parent = rel.RelatingBuildingElement
if parent and self.compare(parent.Name, comparison, value):
parents.add(parent)
for rel in self.file.by_type("IfcRelVoidsElement"):
parent = rel.RelatingBuildingElement
if parent and self.compare(parent.Name, comparison, value):
parents.add(parent)
for rel in self.file.by_type("IfcRelFillsElement"):
parent = rel.RelatingOpeningElement
if parent and self.compare(parent.Name, comparison, value):
parents.add(parent)
children = set()
for parent in parents:
children |= set(ifcopenshell.util.element.get_decomposition(parent))
if comparison == "=":
self.elements = self.elements & children
else:
self.elements -= children
def query(self, args):
keys, comparison, value = args
@@ -16,27 +16,45 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import pytest
import test.bootstrap
import ifcopenshell.api
import ifcopenshell.api.resource
import ifcopenshell.util.resource
class TestAddResourceQuantity(test.bootstrap.IFC4):
def test_run(self):
schema = ifcopenshell.schema_by_name(self.file.schema)
quantity_types = [t.name() for t in schema.declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()]
self.file.create_entity("IfcProject") # add_resource
resource = ifcopenshell.api.run("resource.add_resource", self.file, ifc_class="IfcCrewResource")
resource_types = [t.name() for t in schema.declaration_by_name("IfcConstructionResource").subtypes()]
for quantity_type in quantity_types:
quantity = ifcopenshell.api.run(
"resource.add_resource_quantity", self.file, resource=resource, ifc_class=quantity_type
)
assert quantity.is_a(quantity_type)
assert quantity.Name == "Unnamed"
assert quantity[3] == 0.0
# previous quantity is reassigned and removed
assert resource.BaseQuantity == quantity
assert len(self.file.by_type("IfcPhysicalSimpleQuantity")) == 1
self.file.create_entity("IfcProject") # add_resource
for resource_type in resource_types:
resource = ifcopenshell.api.resource.add_resource(self.file, ifc_class=resource_type)
available_quantities = ifcopenshell.util.resource.RESOURCES_TO_QUANTITIES[resource_type]
for quantity_type in quantity_types:
if quantity_type not in available_quantities:
with pytest.raises(ValueError):
quantity = ifcopenshell.api.resource.add_resource_quantity(
self.file, resource=resource, ifc_class=quantity_type
)
continue
else:
quantity = ifcopenshell.api.resource.add_resource_quantity(
self.file, resource=resource, ifc_class=quantity_type
)
assert quantity.is_a(quantity_type)
assert quantity.Name == "Unnamed"
assert quantity[3] == 0.0
# previous quantity is reassigned and removed
assert resource.BaseQuantity == quantity
assert len(self.file.by_type("IfcPhysicalSimpleQuantity")) == 1
ifcopenshell.api.resource.remove_resource(self.file, resource)
class TestAddResourceQuantityIFC2X3(test.bootstrap.IFC2X3, TestAddResourceQuantity):
@@ -0,0 +1,19 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
# remove_resource tests is partially covered by test_add_resource_quantity.
+1 -1
View File
@@ -28,7 +28,7 @@ from typing import Union
def deprecation_check(test):
def new_test(self):
assert datetime.now().date() < datetime(2024, 6, 1).date(), "API arguments are completely deprecated"
assert datetime.now().date() < datetime(2024, 8, 1).date(), "API arguments are completely deprecated"
test(self)
return new_test
+10 -1
View File
@@ -16,13 +16,22 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import pytest
import test.bootstrap
import ifcopenshell.api
import ifcopenshell.util.unit as subject
from math import pi
class TestConvert(test.bootstrap.IFC4):
def test_run(self):
assert subject.convert(1, None, "METRE", None, "METRE") == 1
assert subject.convert(1, None, "METRE", "MILLI", "METRE") == 1000
assert subject.convert(1000, "MILLI", "METRE", None, "METRE") == 1
assert subject.convert(1, None, "SQUARE_METRE", None, "SQUARE_METRE") == 1
assert subject.convert(1, None, "SQUARE_METRE", "MILLI", "SQUARE_METRE") == 1000000
assert subject.convert(1, None, "CUBIC_METRE", "MILLI", "CUBIC_METRE") == 1000000000
class TestCalculateUnitScale(test.bootstrap.IFC4):
def test_prefix_and_conversion_based_units_are_considered(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")