Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0

This commit is contained in:
Carlos Dias
2023-08-14 22:10:54 -03:00
committed by GitHub
90 changed files with 2283 additions and 614 deletions
+1
View File
@@ -4,6 +4,7 @@ set MY_PY_VER=%PY_VER:.=%
set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib"
cmake -G "Ninja" ^
-D SCHEMA_VERSIONS="2x3;4;4x1;4x3;4x3_add1" ^
-D CMAKE_BUILD_TYPE:STRING=Release ^
-D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^
-D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
+1
View File
@@ -13,6 +13,7 @@ if [ `uname` == Darwin ]; then
fi
cmake -G Ninja \
-DSCHEMA_VERSIONS="2x3;4;4x1;4x3;4x3_add1" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
${CMAKE_PLATFORM_FLAGS[@]} \
+3 -3
View File
@@ -36,10 +36,10 @@ requirements:
run:
- python
- boost-cpp
- occt ==7.7.0
- {{ pin_compatible('occt', max_pin='x.x.x') }}
- {{ pin_compatible('cgal-cpp', max_pin='x.x.x') }}
- {{ pin_compatible('boost-cpp', max_pin='x.x.x') }}
- libxml2
- cgal-cpp
- hdf5
- mpfr
- gmp # [unix]
@@ -145,6 +145,8 @@ classes = [
ui.BIM_PT_tab_services_object,
# Structural analysis
ui.BIM_PT_tab_structural,
# Construction scheduling
ui.BIM_PT_tab_4D5D,
# Facility management
ui.BIM_PT_tab_handover,
ui.BIM_PT_tab_operations,
@@ -35,7 +35,6 @@ classes = (
operator.ViewBrickItem,
operator.SerializeBrick,
operator.AddBrickNamespace,
operator.SetBrickListRoot,
operator.RemoveBrickRelation,
prop.Brick,
prop.BIMBrickProperties,
@@ -37,6 +37,7 @@ class LoadBrickProject(bpy.types.Operator, Operator):
bl_idname = "bim.load_brick_project"
bl_label = "Load Brickschema Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load in a Brick project from a file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
@@ -56,6 +57,7 @@ class ViewBrickClass(bpy.types.Operator, Operator):
bl_idname = "bim.view_brick_class"
bl_label = "View Brick Class"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Inspect the subclasses of this class"
brick_class: bpy.props.StringProperty(name="Brick Class")
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
@@ -67,6 +69,7 @@ class ViewBrickItem(bpy.types.Operator, Operator):
bl_idname = "bim.view_brick_item"
bl_label = "View Brick Item"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Inspect this entity in the viewer"
item: bpy.props.StringProperty(name="Brick Item")
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
@@ -78,6 +81,7 @@ class RewindBrickClass(bpy.types.Operator, Operator):
bl_idname = "bim.rewind_brick_class"
bl_label = "Rewind Brick Class"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Go back to the previous list view"
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
def _execute(self, context):
@@ -88,6 +92,7 @@ class CloseBrickProject(bpy.types.Operator, Operator):
bl_idname = "bim.close_brick_project"
bl_label = "Close Brick Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Close the Brick project"
def _execute(self, context):
core.close_brick_project(tool.Brick)
@@ -122,6 +127,7 @@ class AddBrick(bpy.types.Operator, Operator):
bl_idname = "bim.add_brick"
bl_label = "Add Brick"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create the Brick entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
@@ -143,6 +149,7 @@ class AddBrickRelation(bpy.types.Operator, Operator):
bl_idname = "bim.add_brick_relation"
bl_label = "Add Brick Relation"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create the Brick relationship"
def _execute(self, context):
props = context.scene.BIMBrickProperties
@@ -178,6 +185,7 @@ class NewBrickFile(bpy.types.Operator):
bl_idname = "bim.new_brick_file"
bl_label = "New Brick File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create a Brick project from scratch"
def execute(self, context):
IfcStore.begin_transaction(self)
@@ -210,16 +218,18 @@ class RefreshBrickViewer(bpy.types.Operator, Operator):
bl_idname = "bim.refresh_brick_viewer"
bl_label = "Refresh Brick Viewer"
bl_options = {"REGISTER", "UNDO"}
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
bl_description = "Refresh the list view"
def _execute(self, context):
core.refresh_brick_viewer(tool.Brick, split_screen=self.split_screen)
core.refresh_brick_viewer(tool.Brick)
core.refresh_brick_viewer(tool.Brick, split_screen=True)
class RemoveBrick(bpy.types.Operator, Operator):
bl_idname = "bim.remove_brick"
bl_label = "Remove Brick"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Delete this entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
@@ -262,6 +272,7 @@ class SerializeBrick(bpy.types.Operator):
class AddBrickNamespace(bpy.types.Operator, Operator):
bl_idname = "bim.add_brick_namespace"
bl_label = "Add Brick Namespace"
bl_description = "Bind a new namespace to the Brick project"
def _execute(self, context):
props = context.scene.BIMBrickProperties
@@ -270,24 +281,11 @@ class AddBrickNamespace(bpy.types.Operator, Operator):
core.add_namespace(tool.Brick, alias=alias, uri=uri)
class SetBrickListRoot(bpy.types.Operator, Operator):
bl_idname = "bim.set_brick_list_root"
bl_label = "Set Brick View Type"
bl_options = {"REGISTER", "UNDO"}
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
def _execute(self, context):
if self.split_screen:
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
else:
root = context.scene.BIMBrickProperties.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=self.split_screen)
class RemoveBrickRelation(bpy.types.Operator, Operator):
bl_idname = "bim.remove_brick_relation"
bl_label = "Remove Relation"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Delete this relationship"
predicate: bpy.props.StringProperty(name="Relation")
object: bpy.props.StringProperty(name="Object")
@@ -30,6 +30,8 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
import blenderbim.core.brick as core
import blenderbim.tool.brick as tool
from blenderbim.tool.brick import BrickStore
def update_active_brick_index(self, context):
@@ -65,6 +67,15 @@ def get_brick_relations(self, context):
return BrickStore.relationships
def update_view(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
def split_screen_update_view(self, context):
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
class Brick(PropertyGroup):
name: StringProperty(name="Name")
label: StringProperty(name="Label")
@@ -79,7 +90,7 @@ class BIMBrickProperties(PropertyGroup):
active_brick_index: IntProperty(name="Active Brick Index", update=update_active_brick_index)
libraries: EnumProperty(name="Libraries", items=get_libraries)
set_list_root_toggled: BoolProperty(name="Set List Root Toggled", default=False)
brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots)
brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots, update=update_view)
# namespace manager
namespace: EnumProperty(name="Namespace", items=get_namespaces)
brick_settings_toggled: BoolProperty(name="Brick Settings Toggled", default=False)
@@ -102,4 +113,4 @@ class BIMBrickProperties(PropertyGroup):
split_screen_active_brick_index: IntProperty(name="Split Screen Active Brick Index", update=update_active_brick_index)
split_screen_active_brick_class: StringProperty(name="Split Screen Active Brick Class")
split_screen_brick_breadcrumbs: CollectionProperty(name="Split Screen Brick Breadcrumbs", type=StrProperty)
split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots)
split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view)
@@ -42,9 +42,17 @@ class BIM_PT_brickschema(Panel):
row.operator("bim.load_brick_project", text="Load Project")
return
row = self.layout.row(align=True)
if BrickStore.path:
row = self.layout.row(align=True)
row.label(text=BrickStore.path, icon="FILEBROWSER")
else:
row.label(text="No file", icon="FILEBROWSER")
row = self.layout.row(align=True)
if BrickStore.last_saved:
row.label(text=BrickStore.last_saved, icon="TIME")
else:
row.label(text="Not saved", icon="TIME")
row = self.layout.row(align=True)
op = row.operator("bim.serialize_brick", icon="EXPORT", text="Save")
@@ -55,7 +63,7 @@ class BIM_PT_brickschema(Panel):
row = self.layout.row(align=True)
row.prop(data=self.props, property="brick_settings_toggled", text="", icon="PREFERENCES")
if self.props.brick_settings_toggled:
box = self.layout.box()
row = box.row(align=True)
@@ -86,47 +94,43 @@ class BIM_PT_brickschema(Panel):
row.prop(data=self.props, property="new_brick_label", text="")
prop_with_search(row, self.props, "brick_entity_class", text="")
row.operator("bim.add_brick", text="", icon="ADD")
# row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH")
row = self.layout.row(align=True)
col = row.column()
col.alignment = "RIGHT"
row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER")
row.prop(data=self.props, property="split_screen_toggled", text="", icon="WINDOW")
row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH")
grid = self.layout.grid_flow(even_columns=True)
grid1 = grid.column(align=True)
row = grid1.row(align=True)
grid_left = grid.column(align=True)
row = grid_left.row(align=True)
if len(self.props.brick_breadcrumbs):
op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV")
op.split_screen = False
row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER")
row.label(text=self.props.active_brick_class)
if self.props.set_list_root_toggled:
row = grid1.row(align=True)
op = row.operator("bim.set_brick_list_root", text="Set View")
op.split_screen = False
row = grid_left.row(align=True)
row.prop(data=self.props, property="brick_list_root", text="")
row = grid1.row()
row = grid_left.row()
BIM_UL_bricks.split_screen = False
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
if self.props.split_screen_toggled:
grid2 = grid.column(align=True)
row = grid2.row(align=True)
grid_right = grid.column(align=True)
row = grid_right.row(align=True)
if len(self.props.split_screen_brick_breadcrumbs):
op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV")
op.split_screen = True
row.label(text=self.props.split_screen_active_brick_class)
if self.props.set_list_root_toggled:
row = grid2.row(align=True)
op = row.operator("bim.set_brick_list_root", text="Set View")
op.split_screen = True
row = grid_right.row(align=True)
row.prop(data=self.props, property="split_screen_brick_list_root", text="")
row = grid2.row()
row = grid_right.row()
BIM_UL_bricks.split_screen = True
row.template_list("BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index")
@@ -165,12 +169,10 @@ class BIM_PT_brickschema(Panel):
prop_with_search(row, self.props, "new_brick_relation_type", text="")
row.prop(data=self.props, property="new_brick_relation_object", text="")
row.operator("bim.add_brick_relation", text="", icon="ADD")
if self.props.brick_create_relations_toggled and self.props.add_relation_failed:
row = self.layout.row(align=True)
row.label(text="Failed to find this entity!", icon="ERROR")
for relation in BrickschemaData.data["active_relations"]:
row = self.layout.row(align=True)
@@ -20,64 +20,65 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AddCostColumn,
operator.AddCostItem,
operator.AddCostItemQuantity,
operator.AddCostSchedule,
operator.RemoveCostSchedule,
operator.EditCostSchedule,
operator.AddCostValue,
operator.AddCurrency,
operator.AddSummaryCostItem,
operator.AssignCostItemQuantity,
operator.AssignCostItemType,
operator.AssignCostValue,
operator.CalculateCostItemResourceValue,
operator.ChangeParentCostItem,
operator.ClearCostItemAssignments,
operator.ContractCostItem,
operator.ContractCostItemRate,
operator.ContractCostItems,
operator.CopyCostItem,
operator.CopyCostItemValues,
operator.DisableEditingCostItem,
operator.DisableEditingCostItemQuantity,
operator.DisableEditingCostItemValue,
operator.DisableEditingCostSchedule,
operator.EditCostItem,
operator.EditCostItemQuantity,
operator.EditCostItemValue,
operator.EditCostItemValueFormula,
operator.EnableEditingCostSchedule,
operator.EnableEditingCostItems,
operator.EditCostSchedule,
operator.EnableEditingCostItem,
operator.ExportCostSchedules,
operator.ExpandCostItems,
operator.EnableEditingCostItemQuantities,
operator.EnableEditingCostItemQuantity,
operator.EnableEditingCostItemValues,
operator.EnableEditingCostItems,
operator.EnableEditingCostItemValue,
operator.EnableEditingCostItemValueFormula,
operator.DisableEditingCostItem,
operator.DisableEditingCostSchedule,
operator.DisableEditingCostItemQuantity,
operator.DisableEditingCostItemValue,
operator.AddCostColumn,
operator.RemoveCostColumn,
operator.AddCostItem,
operator.AddSummaryCostItem,
operator.EnableEditingCostItemValues,
operator.EnableEditingCostSchedule,
operator.ExpandCostItem,
operator.ContractCostItem,
operator.ExpandCostItemRate,
operator.ExpandCostItems,
operator.ExportCostSchedules,
operator.HighlightProductCostItem,
operator.ImportCostScheduleCsv,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemQuantities,
operator.LoadCostItemResourceQuantities,
operator.LoadCostItemTaskQuantities,
operator.LoadCostItemTypes,
operator.LoadProductCostItems,
operator.LoadScheduleOfRates,
operator.RemoveCostColumn,
operator.RemoveCostItem,
operator.AssignCostItemType,
operator.UnassignCostItemType,
operator.AssignCostItemQuantity,
operator.UnassignCostItemQuantity,
operator.AddCostItemQuantity,
operator.RemoveCostItemQuantity,
operator.AddCostValue,
operator.RemoveCostItemValue,
operator.CopyCostItemValues,
operator.RemoveCostSchedule,
operator.ReorderCostItem,
operator.SelectCostItemProducts,
operator.SelectCostScheduleProducts,
operator.ImportCostScheduleCsv,
operator.LoadCostItemQuantities,
operator.LoadCostItemTypes,
operator.AssignCostValue,
operator.LoadScheduleOfRates,
operator.ExpandCostItemRate,
operator.ContractCostItemRate,
operator.CalculateCostItemResourceValue,
operator.ClearCostItemAssignments,
operator.HighlightProductCostItem,
operator.LoadProductCostItems,
operator.ReorderCostItem,
operator.SelectUnassignedProducts,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemTaskQuantities,
operator.LoadCostItemResourceQuantities,
operator.ChangeParentCostItem,
operator.CopyCostItem,
operator.AddCurrency,
operator.UnassignCostItemQuantity,
operator.UnassignCostItemType,
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
@@ -117,6 +117,7 @@ class CostSchedulesData:
data["TotalAppliedValue"] = 0.0
data["TotalCost"] = 0.0
has_unit_basis = False
is_sum = False
if root_element.is_a("IfcCostItem"):
values = root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
@@ -130,6 +131,11 @@ class CostSchedulesData:
data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"]
data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"]
has_unit_basis = True
else:
data["UnitBasisValueComponent"] = 1
data["UnitBasisUnitSymbol"] = "U"
if cost_value.Category == "*":
is_sum = True
if has_unit_basis:
data["TotalCost"] = data["TotalAppliedValue"] / data["UnitBasisValueComponent"]
else:
@@ -137,7 +143,8 @@ class CostSchedulesData:
data["TotalCost"] = data["TotalAppliedValue"] * data["TotalCostQuantity"]
else:
data["TotalCost"] = data["TotalAppliedValue"]
data["TotalAppliedValue"] = None
if is_sum:
data["TotalAppliedValue"] = None
@classmethod
def _load_cost_item_quantities(cls, cost_item, data):
@@ -154,7 +161,7 @@ class CostSchedulesData:
if unit:
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
else:
data["UnitSymbol"] = None
data["UnitSymbol"] = "U"
# same_unit_nested_cost_item = set()
# data["DerivedTotalCostQuantity"] = None
@@ -156,6 +156,17 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
core.contract_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.contract_cost_items"
bl_label = "Contract Cost Item"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Collapse cost item tree"
cost_item: bpy.props.IntProperty()
def _execute(self, context):
core.contract_cost_items(tool.Cost)
class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_cost_item"
bl_label = "Remove Cost Item"
+23 -27
View File
@@ -21,7 +21,6 @@ import blenderbim.bim.module.cost.prop as CostProp
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.cost.data import CostSchedulesData
import blenderbim.tool as tool
class BIM_PT_cost_schedules(Panel):
@@ -30,10 +29,12 @@ class BIM_PT_cost_schedules(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_4D5D"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3"
file = IfcStore.get_file()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
if not CostSchedulesData.is_loaded:
@@ -46,8 +47,8 @@ class BIM_PT_cost_schedules(Panel):
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT")
else:
row.label(text="No Cost Schedules found.", icon="COMMUNITY")
row = self.layout.row()
row.label(text="No Cost Schedules found.", icon="TEXT")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "cost_schedule_predefined_types")
row.operator("bim.add_cost_schedule", icon="ADD", text="Add")
@@ -138,8 +139,8 @@ class BIM_PT_cost_schedules(Panel):
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.add_summary_cost_item", text="Add Summary Cost", icon="ADD")
row.operator("bim.expand_all_tasks", text="Expand All")
row.operator("bim.contract_all_tasks", text="Contract All")
row.operator("bim.expand_cost_items", text="Expand All")
row.operator("bim.contract_cost_items", text="Contract All")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items):
@@ -619,18 +620,22 @@ class BIM_UL_cost_items_trait:
else:
row.label(text="", icon="DOT")
def draw_total_cost_column(self, layout, cost_item):
format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ")
currency = CostSchedulesData.data["currency"]
text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers
layout.label(text=text)
def draw_quantity_column(self, layout, cost_item):
if CostSchedulesData.data["is_editing_rates"]:
self.draw_uom_column(layout, cost_item)
else:
self.draw_total_quantity_column(layout, cost_item)
def draw_uom_column(self, layout, cost_item):
layout.label(text=cost_item["UnitBasisUnitSymbol"])
def draw_total_quantity_column(self, layout, cost_item):
if cost_item["TotalCostQuantity"]:
label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}"
layout.label(text=label)
else:
layout.label(text="-")
def draw_value_column(self, layout, cost_item):
if cost_item["TotalAppliedValue"]:
text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ")
@@ -642,8 +647,11 @@ class BIM_UL_cost_items_trait:
else:
layout.label(text="-")
def draw_uom_column(self, layout, cost_item):
layout.label(text=cost_item["UnitBasisUnitSymbol"] or "-" if cost_item["UnitBasisValueComponent"] else "-")
def draw_total_cost_column(self, layout, cost_item):
format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ")
currency = CostSchedulesData.data["currency"]
text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers
layout.label(text=text)
def draw_order_operator(self, row, ifc_definition_id, cost_item):
if cost_item["NestingIndex"] is not None:
@@ -656,19 +664,7 @@ class BIM_UL_cost_items_trait:
op.cost_item = ifc_definition_id
op.new_index = cost_item["NestingIndex"] - 1
def draw_total_quantity_column(self, layout, cost_item):
if cost_item["TotalCostQuantity"]:
label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}"
layout.label(text=label)
else:
layout.label(text="-")
# if cost_item["DerivedTotalCostQuantity"] not in [None, 0]:
# layout.label(text="{0:.2f}".format(cost_item["DerivedTotalCostQuantity"]) + f" {cost_item['DerivedUnitSymbol'] or '-'}")
# else:
# if cost_item["TotalCostQuantity"] == 0:
# layout.label(text="-")
# else:
# layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}")
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
@@ -129,9 +129,8 @@ class ExportCsvAttributes(bpy.types.Operator):
class ExportIfcCsv(bpy.types.Operator):
bl_idname = "bim.export_ifccsv"
bl_label = "Export IFC"
#filename_ext = ".csv"
filename_ext = ".csv"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
props = context.scene.CsvProperties
@@ -149,8 +148,7 @@ class ExportIfcCsv(bpy.types.Operator):
ifc_file = IfcStore.get_file()
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
selector = ifcopenshell.util.selector.Selector()
results = selector.parse(ifc_file, props.ifc_selector)
results = ifcopenshell.util.selector.filter_elements(ifc_file, props.ifc_selector)
ifc_csv = ifccsv.IfcCsv()
attributes = [a.name for a in props.csv_attributes]
sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter
@@ -196,9 +194,12 @@ class EyedropIfcCsv(bpy.types.Operator):
global_ids = []
self.file = IfcStore.get_file()
for obj in context.selected_objects:
if hasattr(obj, "BIMObjectProperties") and obj.BIMObjectProperties.ifc_definition_id:
global_ids.append("#" + self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId)
context.scene.CsvProperties.ifc_selector = "|".join(global_ids)
element = tool.Ifc.get_entity(obj)
if element:
global_id = getattr(element, "GlobalId", None)
if global_id:
global_ids.append(global_id)
context.scene.CsvProperties.ifc_selector = ",".join(global_ids)
return {"FINISHED"}
@@ -154,6 +154,8 @@ class ValidateIfcFile(bpy.types.Operator):
logger = logging.getLogger("validate")
logger.setLevel(logging.DEBUG)
ifcopenshell.validate.validate(IfcStore.get_file(), logger, express_rules=True)
self.report({"INFO"}, "Check validation results in the system console.")
return {"FINISHED"}
@@ -166,6 +166,7 @@ class Annotator:
center = camera.matrix_world.inverted() @ bpy.context.scene.cursor.location
center.z = 0
center = camera.matrix_world @ center
return (
center + z_offset,
@@ -248,8 +248,13 @@ class BaseDecorator:
if check_mode and obj.data.is_editmode:
return self.get_editmesh_geom(obj)
vertices = [obj.matrix_world @ v.co for v in obj.data.vertices]
indices = [e.vertices for e in obj.data.edges]
bm = bmesh.new()
bm.from_mesh(obj.data)
vertices = [obj.matrix_world @ v.co for v in bm.verts]
# In object mode, it's nicer to not show "internal edges". Most will be dissolved anyway.
indices = [[v.index for v in e.verts] for e in bm.edges if len(e.link_faces) != 2]
bm.free()
return vertices, indices
def get_editmesh_geom(self, obj):
@@ -520,7 +525,11 @@ class BaseDecorator:
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(obj.matrix_world)
text_dir = (camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
camera_matrix = camera.matrix_world.copy()
camera_matrix[0][0] = 1
camera_matrix[1][1] = 1
camera_matrix[2][2] = 1
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
pos = location_3d_to_region_2d(region, region3d, text_world_position)
props = obj.BIMTextProperties
@@ -485,7 +485,7 @@ class CreateDrawing(bpy.types.Operator):
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc)
self.setup_serialiser(ifc, target_view)
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
tree = ifcopenshell.geom.tree()
@@ -829,7 +829,7 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def setup_serialiser(self, ifc):
def setup_serialiser(self, ifc, target_view):
self.svg_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
@@ -853,6 +853,8 @@ class CreateDrawing(bpy.types.Operator):
self.serialiser.setScale(self.scale)
self.serialiser.setSubtractionSettings(ifcopenshell.ifcopenshell_wrapper.ALWAYS)
self.serialiser.setUsePrefiltering(True) # See #3359
if target_view == "REFLECTED_PLAN_VIEW":
self.serialiser.setMirrorY(True)
# tree = ifcopenshell.geom.tree()
# This instructs the tree to explode BReps into faces and return
# the style of the face when running tree.select_ray()
@@ -887,6 +889,21 @@ class CreateDrawing(bpy.types.Operator):
)
return classes
def is_manifold(self, obj):
result = self.is_manifold_cache.get(obj.data.name, None)
if result is not None:
return result
bm = bmesh.new()
bm.from_mesh(obj.data)
for edge in bm.edges:
if not edge.is_manifold:
bm.free()
self.is_manifold_cache[obj.data.name] = False
return False
self.is_manifold_cache[obj.data.name] = True
return True
def merge_linework_and_add_metadata(self, root):
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
if join_criteria:
@@ -897,6 +914,7 @@ class CreateDrawing(bpy.types.Operator):
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
joined_paths = {}
self.is_manifold_cache = {}
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
@@ -906,6 +924,10 @@ class CreateDrawing(bpy.types.Operator):
classes.append("cut")
el.set("class", " ".join(classes))
obj = tool.Ifc.get_object(element)
if not self.is_manifold(obj):
continue
# An element group will contain a bunch of paths representing the
# cut of that element. However IfcOpenShell may not correctly
# create closed paths. We post-process all paths with shapely to
@@ -1512,6 +1534,9 @@ class RemoveDrawing(bpy.types.Operator, Operator):
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
]
else:
if not self.drawing:
self.report({"ERROR"}, "No drawing selected")
return {"CANCELLED"}
drawings = [tool.Ifc.get().by_id(self.drawing)]
removed_drawings = [drawing.id() for drawing in drawings]
@@ -1862,7 +1887,7 @@ class RemoveSchedule(bpy.types.Operator, Operator):
schedule: bpy.props.IntProperty()
def _execute(self, context):
core.remove_document(tool.Ifc, tool.Drawing, "SCHEDULE", schedule=tool.Ifc.get().by_id(self.schedule))
core.remove_document(tool.Ifc, tool.Drawing, "SCHEDULE", document=tool.Ifc.get().by_id(self.schedule))
class OpenSchedule(bpy.types.Operator, Operator):
@@ -118,6 +118,25 @@ class Scheduler:
if cell_style:
related_styles.append((style_name, cell_style))
# sometimes there are no column styles (e.g. in ODS from IfcCSV)
# and we just use some constant number for column widths
if not column_widths:
row_columns = []
for tr in table.getElementsByType(TableRow):
row_cols = 0
for td in tr.getElementsByType(TableCell):
column_span = td.getAttribute("numbercolumnsspanned")
column_span = int(column_span) if column_span else 1
col_repeat = td.getAttribute("numbercolumnsrepeated")
col_repeat = int(col_repeat) if col_repeat else 1
row_cols += column_span * col_repeat
row_columns.append(row_cols)
n_columns = max(row_columns)
column_widths = [25] * n_columns # some constant width value 👀
column_styles = [None] * n_columns
# collect rows height
row_heights = []
# TODO: never used yet because unsure about priority for row styles
@@ -351,7 +370,7 @@ class Scheduler:
wrap_text: if True, text will be wrapped to fit in cell
cell_width: width of cell, used for wrapping text
"""
text_lines = [str(p).upper() for p in p_tags]
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {
"font-size": font_size,
@@ -791,7 +791,14 @@ class SvgWriter:
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world)
text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
# RCP cameras may be scaled, so reset scales.
camera_matrix = self.camera.matrix_world.copy()
camera_matrix[0][0] = 1
camera_matrix[1][1] = 1
camera_matrix[2][2] = 1
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
classes = self.get_attribute_classes(text_obj)
@@ -314,7 +314,7 @@ class BIM_PT_references(Panel):
if not self.props.is_editing_references:
row = self.layout.row(align=True)
row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="LONGDISPLAY")
row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="OBJECT_HIDDEN")
row.operator("bim.load_references", text="", icon="IMPORT")
return
@@ -637,6 +637,11 @@ class OverrideDuplicateMove(bpy.types.Operator):
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
continue # For now, don't copy drawings until we stabilise a bit more. It's tricky.
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
@@ -705,6 +710,10 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
old_to_new = {}
for obj in context.selected_objects:
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
@@ -34,6 +34,7 @@ classes = (
operator.GetCursorLocation,
operator.SetCursorLocation,
operator.ConvertAngleToCoordinates,
operator.ImportPlot,
prop.BIMGeoreferenceProperties,
ui.BIM_PT_gis,
ui.BIM_PT_gis_utilities,
@@ -110,7 +110,7 @@ class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.set_cursor_location"
bl_label = "Set Cursor Location"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Move curson location to the specified coordinates"
bl_description = "Move cursor location to the specified coordinates"
@classmethod
def poll(cls, context):
@@ -187,3 +187,20 @@ class ConvertAngleToCoordinates(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.convert_angle_to_coord(tool.Georeference, type=self.type)
class ImportPlot(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.import_plot"
bl_label = "Import Plot"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import plot"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
def execute(self, context):
core.import_plot(tool.Georeference, filepath=self.filepath)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -50,8 +50,9 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
row.operator("bim.clean_wireframes")
row = layout.row()
row.operator("bim.patch_non_parametric_mep_segment")
row = layout.row(align=True)
row.operator("bim.enable_editing_sketch_extrusion_profile", text="Start Sketching")
row.operator("bim.edit_sketch_extrusion_profile", text="", icon="FILE_REFRESH")
row.operator("bim.disable_editing_sketch_extrusion_profile", text="", icon="CANCEL")
row = layout.row()
row.operator("bim.import_plot", text="Import Plot Coordinates", icon="FILE_FOLDER")
@@ -177,6 +177,7 @@ classes = (
roof.RemoveRoof,
roof.SetGableRoofEdgeAngle,
mep.MEPAddObstruction,
mep.MEPAddTransition,
)
addon_keymaps = []
+319 -59
View File
@@ -18,7 +18,10 @@
import bpy
import math
import collections
import bmesh
import re
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
@@ -31,13 +34,13 @@ import blenderbim.core.type
import blenderbim.core.root
import blenderbim.core.geometry
import blenderbim.tool as tool
from math import pi, degrees
from math import pi, degrees, radians
from copy import copy
from mathutils import Vector, Matrix
import re
from ifcopenshell.util.shape_builder import ShapeBuilder
from blenderbim.bim.module.model.profile import DumbProfileJoiner
V = lambda *x: Vector([float(i) for i in x])
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
@@ -70,29 +73,68 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
[e for e in ifcopenshell.util.system.get_connected_from(element) if e not in processed_elements]
)
if len(connected) == 1:
extend_branch(list(connected)[0], branch, element)
else:
for connected_element in connected:
branch_element["children"].append(extend_branch(connected_element, [], element))
for connected_element in connected:
branch_element["children"].append(extend_branch(connected_element, [], element))
return branch
queue = extend_branch(current_element, [])[0]["children"]
extended_branch = extend_branch(current_element, [])
queue = extended_branch[0]["children"]
# import pprint
# pprint.pprint(queue)
def get_connected_ports_between(element1, element2):
ports1 = tool.System.get_ports(element1)
ports2 = tool.System.get_ports(element2)
for p in ports1:
connected_port = tool.System.get_connected_port(p)
# in IFC2X3 there is no PredefinedType
if getattr(p, "PredefinedType", None) == "WIRELESS":
continue
if connected_port in ports2:
return p, connected_port
return None, None
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def process_branch(branch):
for branch_element in branch:
element = branch_element["element"]
print('processing', element)
print("processing", element)
predecessor = branch_element["predecessor"]
if False: # If the element does not need to be transformed, return early.
return
# Perform the extend, translate, rotate, etc the element as necessary based on the predecessor.
# For segments, prioritise extensions instead of translations.
# For everything else, only translate. No rotation.
# For everything besides segments, only translate. No rotation.
obj = tool.Ifc.get_object(element)
obj_pred = tool.Ifc.get_object(predecessor)
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.is_moved(obj_pred):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj_pred)
port, port_pred = get_connected_ports_between(element, predecessor)
port_matrix_pred = tool.Model.get_element_matrix(port_pred)
# Only segments can be extended
# extension for them takes priority over translation
if element.is_a("IfcFlowSegment"):
DumbProfileJoiner().join_E(obj, port_matrix_pred.translation * si_conversion)
context.view_layer.update() # update since extrusion might involve changing object's location
port_martix = tool.Model.get_element_matrix(port)
port_location = port_martix.translation
port_location_pred = port_matrix_pred.translation
if not tool.Cad.are_vectors_equal(port_location, port_location_pred):
obj.location += (port_location_pred - port_location) * si_conversion
context.view_layer.update() # otherwise tool.Ifc.is_moved won't get triggered
else:
# If the element does not need to be transformed, return early.
return
for child_branch in branch_element["children"]:
process_branch(child_branch)
@@ -107,6 +149,9 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
# TODO: need to add ui for parameters:
# - obstruction cap thickness
# - start/end thickness and angle for transition
selected_objs = []
selected_profiles = []
@@ -162,6 +207,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
elif total_profiles == 2:
if is_parallel:
fitting_type = "TRANSITION"
bpy.ops.bim.mep_add_transition()
elif total_selected_objs == 3:
if total_profiles > 1:
@@ -207,12 +253,7 @@ class MEPGenerator:
ports = tool.System.get_ports(segment)
if segment.is_a("IfcFlowSegment") and not ports:
for mat in [start_port_matrix, end_port_matrix]:
# TODO: specify PredefinedType based on the segment type
port = tool.Ifc.run("system.add_port", element=segment)
port.FlowDirection = "NOTDEFINED"
port.PredefinedType = self.get_port_predefined_type(segment)
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=mat, is_si=True)
tool.System.add_ports(obj)
return
# adjust current segment ports and related flow segments
@@ -228,6 +269,10 @@ class MEPGenerator:
if port_position == "end_port":
tool.Model.edit_element_placement(port, end_port_matrix)
continue
# NOTE: currently this functionality is moved to bim.regenerate_distribution_element
connected_port = tool.System.get_connected_port(port)
if not connected_port:
continue
@@ -248,7 +293,6 @@ class MEPGenerator:
):
if port_position == "start_port":
if segment.is_a("IfcFlowFitting"):
profile_joiner = DumbProfileJoiner()
connected_element_length = (
tool.Model.get_flow_segment_axis(connected_obj)[0]
- tool.Model.get_flow_segment_axis(obj)[0]
@@ -269,45 +313,103 @@ class MEPGenerator:
extrusion_depth = segment_object.dimensions.z
end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth)
segment_data = {
"start_point": start_point,
"end_point": end_point,
"start_point": start_point.copy().freeze(),
"end_point": end_point.freeze(),
"ports": ports,
"extrusion_depth": extrusion_depth,
}
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if float_is_zero(port_local_position.length):
if tool.Cad.is_x(port_local_position.length, 0.0):
segment_data["start_port"] = port
else:
segment_data["end_port"] = port
return segment_data
def get_port_predefined_type(self, segment):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(segment.is_a())[1:-1]).upper()
if class_name == "CONVEYOR":
return "NOTDEFINED"
return class_name
def get_mep_element_class_name(self, element, mep_class_type):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type])
return class_name
def get_compatible_fitting_type(self, segment, predefined_type):
"""We find compatible fitting only by checking if they were
already used with that segment type before.
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type):
"""
returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting.
We find compatible fitting only by checking
if they were already used with that segment type before
and fitting's ports should match `port_or_ports` by PredefinedType and SystemType.
If port from `port_or_ports` has PredefinedType/SystemType == None/NOTDEFINED then
those parameters won't be taken into account checking compatibility.
There lies the problem that it won't be
able to identify the fittings that were not connected to any segments yet.
able to identify the fittings that were not yet connected to any segments yet.
"""
segment_type = ifcopenshell.util.element.get_type(segment)
if not segment_type:
return None
if not isinstance(segment_or_segments, collections.abc.Iterable):
segments = [segment_or_segments]
ports = [port_or_ports]
else:
segments = segment_or_segments
ports = port_or_ports
fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segment, "Fitting"))
segments_data = []
for segment, port in zip(segments, ports, strict=True):
segment_type = ifcopenshell.util.element.get_type(segment)
# if segment doesn't have type we cannot check compatibility by available occurences
if segment_type is None:
return
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
def are_connected_elements_compatible(segments_data, fitting_data):
# prevent arguments mutation, not using deepcopy because of the errors with ifc elements
segments_data = [copy(i) for i in segments_data]
fitting_data = [copy(i) for i in fitting_data]
not_defined_values = {"NOTDEFINED", None}
if len(segments_data) != len(fitting_data):
return False
def are_segments_compatible(test_segment_data, base_segment_data):
segment_type, predefined_type, system_type = test_segment_data
base_segment_type, base_predefined_type, base_system_type = base_segment_data
if segment_type != base_segment_type:
return False
if predefined_type not in not_defined_values and predefined_type != base_predefined_type:
return False
if system_type not in not_defined_values and system_type != base_system_type:
return False
return True
# NOTE: I have a feeling that there are cases where order
# in which we're checking the segments is important
# but I couldn't pin it down exact cases
for test_segment_data in fitting_data[:]:
for base_segment_data in segments_data:
if not are_segments_compatible(test_segment_data, base_segment_data):
continue
segments_data.remove(test_segment_data)
# all segments were sorted
return len(segments_data) == 0
def pack_return_data(fitting_type, ports, segments_data):
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if tool.Cad.is_x(port_local_position.length, 0.0):
start_port = port
break
connected_port = tool.System.get_connected_port(start_port)
connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
return {"fitting_type": fitting_type, "start_port_match": element_type == segments_data[0][0]}
fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segments[0], "FittingType"))
for fitting_type in fitting_types:
if fitting_type.PredefinedType != predefined_type:
continue
@@ -315,13 +417,24 @@ class MEPGenerator:
if not fittings:
continue
fitting = fittings[0]
elements = set(
ifcopenshell.util.system.get_connected_to(fitting)
+ ifcopenshell.util.system.get_connected_from(fitting)
)
for element in elements:
if element.IsTypedBy and element.IsTypedBy[0].RelatingType == segment_type:
return fitting_type
ports = ifcopenshell.util.system.get_ports(fitting)
fitting_data = []
fitting_connected_to_none_type = False
for port in ports:
connected_port = tool.System.get_connected_port(port)
connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
if element_type is None:
fitting_connected_to_none_type = True
break
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
if fitting_connected_to_none_type:
continue
if are_connected_elements_compatible(segments_data, fitting_data):
return pack_return_data(fitting_type, ports, segments_data)
def create_obstruction_type(self, segment):
# code is very similar to "bim.add_type"
@@ -333,7 +446,8 @@ class MEPGenerator:
ifc_file = tool.Ifc.get()
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
obj = bpy.data.objects.new("Fitting", None)
obj = bpy.data.objects.new("Obstruction", None)
# TODO: OBSTRUCTION predefined type is available only for IfcDuctFitting and IfcPipeFitting
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -377,7 +491,8 @@ class MEPGenerator:
segment_obj = tool.Ifc.get_object(segment)
segment_matrix = segment_obj.matrix_world
segment_rotation = segment_matrix.to_quaternion()
obstruction_type = self.get_compatible_fitting_type(segment, "OBSTRUCTION")
fitting_data = self.get_compatible_fitting_type(segment, related_port, "OBSTRUCTION")
obstruction_type = fitting_data["fitting_type"] if fitting_data else None
if not obstruction_type:
obstruction_type = self.create_obstruction_type(segment)
@@ -389,17 +504,11 @@ class MEPGenerator:
obstruction_obj.matrix_world = segment_matrix
profile_joiner.set_depth(obstruction_obj, length)
obstruction = tool.Ifc.get_entity(obstruction_obj)
# TODO: specify PredefinedType based on the segment type
obstruction_port = tool.Ifc.run("system.add_port", element=obstruction)
obstruction_port.PredefinedType = self.get_port_predefined_type(obstruction)
port_local_position = Matrix.Translation((0, 0, length)) if at_segment_start else Matrix()
tool.Ifc.run(
"geometry.edit_object_placement",
product=obstruction_port,
matrix=segment_matrix @ port_local_position,
is_si=True,
)
obstruction_port = tool.System.add_ports(
obstruction_obj,
add_start_port=not at_segment_start,
add_end_port=at_segment_start,
)[0]
# change segment length
new_segment_length = segment_data["extrusion_depth"] - length
@@ -411,6 +520,7 @@ class MEPGenerator:
obstruction_obj.location += segment_rotation @ V(0, 0, new_segment_length)
tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="NOTDEFINED")
obstruction = tool.Ifc.get_entity(obstruction_obj)
return obstruction, None
@@ -449,3 +559,153 @@ class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator):
return {"CANCELLED"}
return {"FINISHED"}
class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mep_add_transition"
bl_label = "Add Transition"
bl_description = (
"Adds transition between two MEP elements. Elements are either provided by ID or selected in Blender"
)
bl_options = {"REGISTER", "UNDO"}
start_length: bpy.props.FloatProperty(
name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE"
)
end_length: bpy.props.FloatProperty(
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE"
)
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
def _execute(self, context):
start_element, end_element = None, None
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
if self.start_segment_id and self.end_segment_id:
start_element = ifc_file.by_id(self.start_segment_id)
end_element = ifc_file.by_id(self.end_segment_id)
start_object = tool.Ifc.get_object(start_element)
end_object = tool.Ifc.get_object(end_element)
elif len(context.selected_objects) == 2:
start_object = context.active_object
end_object = next(o for o in context.selected_objects if o != context.active_object)
start_element = tool.Ifc.get_entity(start_object)
end_element = tool.Ifc.get_entity(end_object)
if not start_element or not end_element:
self.report({"ERROR"}, f"Two IFC elements should be selected for the transition")
return {"CANCELLED"}
else:
self.report({"ERROR"}, f"Two IFC elements should be provided for the transition")
return {"CANCELLED"}
# TODO: support IfcFlowTerminal
def is_mep(element):
return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting")
if not is_mep(start_element) or not is_mep(end_element):
self.report(
{"ERROR"},
f"Failed to add transition - some object is not a MEP element: {start_element.is_a()}, {end_element.is_a()}.",
)
return {"CANCELLED"}
start_axis = tool.Model.get_flow_segment_axis(start_object)
end_axis = tool.Model.get_flow_segment_axis(end_object)
# TODO: support cases when segments are partially or completely overlapping each other
if not tool.Cad.are_edges_collinear(start_axis, end_axis):
self.report({"ERROR"}, f"Failed to add transition - non collinear segments are not yet supported.")
return {"CANCELLED"}
start_segment_data = MEPGenerator().get_segment_data(start_element)
end_segment_data = MEPGenerator().get_segment_data(end_element)
end_port = end_segment_data["start_port"]
start_port = start_segment_data["end_port"]
points_ports_map = {
start_segment_data["start_point"]: start_segment_data["start_port"],
start_segment_data["end_point"]: start_segment_data["end_port"],
end_segment_data["start_point"]: end_segment_data["start_port"],
end_segment_data["end_point"]: end_segment_data["end_port"],
}
start_point, end_point = tool.Cad.closest_points(
(start_segment_data["start_point"], start_segment_data["end_point"]),
(end_segment_data["start_point"], end_segment_data["end_point"]),
)
transition_dir = (end_point - start_point).normalized()
start_port = points_ports_map[start_point]
end_port = points_ports_map[end_point]
# add transition representation
builder = ShapeBuilder(ifc_file)
rep, transition_data = builder.mep_transition_shape(
start_element, end_element, self.start_length / si_conversion, self.end_length / si_conversion
)
if not rep:
self.report({"ERROR"}, f"Failed to add transition - this kind of profiles is not yet supported.")
return {"CANCELLED"}
middle_point = (start_point + end_point) / 2
full_transition_length = transition_data["full_transition_length"] * si_conversion
start_segment_extend_point = middle_point - transition_dir * full_transition_length / 2
end_segment_extend_point = middle_point + transition_dir * full_transition_length / 2
DumbProfileJoiner().join_E(start_object, start_segment_extend_point)
DumbProfileJoiner().join_E(end_object, end_segment_extend_point)
fitting_data = MEPGenerator().get_compatible_fitting_type(
[start_element, end_element], [start_port, end_port], "TRANSITION"
)
transition_type = fitting_data["fitting_type"] if fitting_data else None
start_port_match = fitting_data["start_port_match"] if fitting_data else True
if not transition_type:
mesh = bpy.data.meshes.new("Transition")
obj = bpy.data.objects.new("Transition", mesh)
transition_type = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=MEPGenerator().get_mep_element_class_name(start_element, "FittingType"),
predefined_type="TRANSITION",
should_add_representation=False,
)
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
tool.Model.replace_object_ifc_representation(body, obj, rep)
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting")
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(transition_data, default=list)},
)
# NOTE: at this point we loose current blender objects selection
bpy.ops.bim.add_constr_type_instance(relating_type_id=transition_type.id())
transition_obj = bpy.context.active_object
# adjust transition segment rotation and location
transition_obj.matrix_world = start_object.matrix_world
context.view_layer.update()
transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj))
direction_match = tool.Cad.are_vectors_equal(transition_obj_dir, transition_dir)
# if there are no mismatches or everything matches up we don't need to flip the transition
if start_port_match != direction_match:
transition_obj.matrix_world = start_object.matrix_world @ Matrix.Rotation(radians(180), 4, "X")
transition_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point
# add ports and connect them
ports = tool.System.add_ports(transition_obj)
if not start_port_match:
start_port, end_port = end_port, start_port
tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED")
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
return {"FINISHED"}
@@ -40,10 +40,6 @@ from pprint import pprint
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.htm
def float_is_zero(f):
return 0.0001 >= f >= -0.0001
def bm_mesh_clean_up(bm):
# remove internal edges and faces
# adding missing faces so we could rely on `e.is_boundary` later
@@ -99,7 +95,7 @@ def is_valid_roof_footprint(bm):
# should be bmesh to support edit mode
bm.verts.ensure_lookup_table()
base_z = bm.verts[0].co.z
all_verts_same_level = all([float_is_zero(v.co.z - base_z) for v in bm.verts[1:]])
all_verts_same_level = all([tool.Cad.is_x(v.co.z - base_z, 0) for v in bm.verts[1:]])
if not all_verts_same_level:
return (
{"ERROR"},
@@ -202,7 +198,7 @@ def generate_hiped_roof_bmesh(
def find_identical_new_vert(co):
for v in bm.verts:
if float_is_zero((co - v.co).length):
if tool.Cad.is_x((co - v.co).length, 0):
return v
def find_other_polygon_verts(edge):
@@ -234,7 +230,7 @@ def generate_hiped_roof_bmesh(
bottom_chords_to_remove = []
def is_footprint_vert(v):
return float_is_zero(v.co.z - footprint_z)
return tool.Cad.is_x(v.co.z - footprint_z, 0)
def is_footprint_edge(edge):
return all(is_footprint_vert(v) for v in edge.verts)
@@ -326,7 +322,7 @@ def generate_hiped_roof_bmesh(
default_offset_dir = Vector([0, 0, 1]) * roof_thickness
footprint_verts = set()
if not float_is_zero(rafter_edge_angle):
if not tool.Cad.is_x(rafter_edge_angle, 0):
footprint_edges = []
for edge in extruded_edges:
if is_footprint_edge(edge):
@@ -398,7 +394,7 @@ def update_roof_modifier_ifc_data(context):
if not angle_layer:
return False
for edge_angle in angle_layer:
if float_is_zero(edge_angle - pi / 2):
if tool.Cad.is_x(edge_angle - pi / 2, 0):
return True
return False
@@ -95,19 +95,19 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True)
for obj in bpy.context.visible_objects:
element = tool.Ifc.get_entity(obj)
visible_element = tool.Ifc.get_entity(obj)
if (
not element
not visible_element
or obj.type != "MESH"
or not self.is_bounding_class(element)
or not self.is_bounding_class(visible_element)
or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal)
):
continue
old_mesh = None
if element.HasOpenings:
new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, element))
if visible_element.HasOpenings:
new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, visible_element))
old_mesh = obj.data
obj.data = new_mesh
@@ -41,6 +41,7 @@ classes = (
operator.SaveLibraryFile,
operator.SelectLibraryFile,
operator.ToggleFilterCategories,
operator.ToggleLinkSelectability,
operator.ToggleLinkVisibility,
operator.UnassignLibraryDeclaration,
operator.UnlinkIfc,
@@ -880,6 +880,27 @@ class LoadLink(bpy.types.Operator):
return {"FINISHED"}
class ToggleLinkSelectability(bpy.types.Operator):
bl_idname = "bim.toggle_link_selectability"
bl_label = "Toggle Link Selectability"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle selectability"
link: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.BIMProjectProperties
link = props.links.get(self.link)
for collection in self.get_linked_collections():
collection.hide_select = not collection.hide_select
link.is_selectable = not collection.hide_select
return {"FINISHED"}
def get_linked_collections(self):
return [
c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.link
]
class ToggleLinkVisibility(bpy.types.Operator):
bl_idname = "bim.toggle_link_visibility"
bl_label = "Toggle Link Visibility"
@@ -93,6 +93,7 @@ class FilterCategory(PropertyGroup):
class Link(PropertyGroup):
name: StringProperty(name="Name")
is_loaded: BoolProperty(name="Is Loaded", default=False)
is_selectable: BoolProperty(name="Is Selectable", default=True)
is_wireframe: BoolProperty(name="Is Wireframe", default=False)
is_hidden: BoolProperty(name="Is Hidden", default=False)
@@ -350,6 +350,13 @@ class BIM_UL_links(UIList):
row = layout.row(align=True)
if item.is_loaded:
row.label(text=item.name)
op = row.operator(
"bim.toggle_link_selectability",
text="",
icon="RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON",
emboss=False,
)
op.link = item.name
op = row.operator(
"bim.toggle_link_visibility",
text="",
@@ -20,20 +20,20 @@ import blenderbim.bim.helper
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.resource.data import ResourceData
import blenderbim.tool as tool
class BIM_PT_resources(Panel):
bl_label = "Resources"
bl_idname = "BIM_PT_resources"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_4D5D"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3"
file = IfcStore.get_file()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
self.props = context.scene.BIMResourceProperties
@@ -20,12 +20,11 @@ import bpy
from . import ui, prop, operator
classes = (
operator.ExpandAllTasks,
operator.ContractAllTasks,
operator.AddAnimationCamera,
operator.AddSummaryTask,
operator.AddTask,
operator.AddTaskColumn,
operator.AddTaskBars,
operator.AddTaskColumn,
operator.AddTimePeriod,
operator.AddWorkCalendar,
operator.AddWorkPlan,
@@ -42,11 +41,15 @@ classes = (
operator.BlenderBIM_DatePickerSetDate,
operator.BlenderBIM_RedrawDatePicker,
operator.CalculateTaskDuration,
operator.ClearPreviousAnimation,
operator.ContractAllTasks,
operator.ContractTask,
operator.CopyTaskAttribute,
operator.CopyTask,
operator.CopyTaskAttribute,
operator.CreateBaseline,
operator.DisableEditingSequence,
operator.DisableEditingTask,
operator.DisableEditingTaskAnimationColors,
operator.DisableEditingTaskTime,
operator.DisableEditingWorkCalendar,
operator.DisableEditingWorkPlan,
@@ -67,23 +70,27 @@ classes = (
operator.EnableEditingTaskCalendar,
operator.EnableEditingTaskSequence,
operator.EnableEditingTaskTime,
operator.EnableEditingWorkScheduleTasks,
operator.EnableEditingWorkCalendar,
operator.EnableEditingWorkCalendarTimes,
operator.EnableEditingWorkPlan,
operator.EnableEditingWorkPlanSchedules,
operator.EnableEditingWorkSchedule,
operator.EnableEditingWorkScheduleTasks,
operator.EnableEditingWorkTime,
operator.ExpandAllTasks,
operator.ExpandTask,
operator.ExportMSP,
operator.ExportP6,
operator.GenerateGanttChart,
operator.GuessDateRange,
operator.ImportMSP,
operator.HighlightTask,
operator.ImportCSV,
operator.ImportMSP,
operator.ImportP6,
operator.ImportP6XER,
operator.ImportPP,
operator.LoadProductTasks,
operator.LoadTaskAnimationColors,
operator.LoadTaskInputs,
operator.LoadTaskOutputs,
operator.LoadTaskProperties,
@@ -98,10 +105,10 @@ classes = (
operator.RemoveWorkSchedule,
operator.RemoveWorkTime,
operator.ReorderTask,
operator.SelectTaskRelatedProducts,
operator.SelectTaskRelatedInputs,
operator.SelectWorkScheduleProducts,
operator.SelectTaskRelatedProducts,
operator.SelectUnassignedWorkScheduleProducts,
operator.SelectWorkScheduleProducts,
operator.SetTaskSortColumn,
operator.SetupDefaultTaskColumns,
operator.UnassignLagTime,
@@ -113,11 +120,6 @@ classes = (
operator.UnassignWorkSchedule,
operator.VisualiseWorkScheduleDate,
operator.VisualiseWorkScheduleDateRange,
operator.LoadTaskAnimationColors,
operator.DisableEditingTaskAnimationColors,
operator.LoadProductTasks,
operator.HighlightTask,
operator.CreateBaseline,
prop.WorkPlan,
prop.BIMWorkPlanProperties,
prop.Task,
@@ -137,6 +139,7 @@ classes = (
ui.BIM_PT_work_schedules,
ui.BIM_PT_work_calendars,
ui.BIM_PT_task_icom,
ui.BIM_PT_animation_tools,
ui.BIM_UL_task_columns,
ui.BIM_UL_task_inputs,
ui.BIM_UL_task_resources,
@@ -19,7 +19,6 @@
import bpy
import blenderbim.tool as tool
import ifcopenshell
from ifcopenshell.util.doc import get_predefined_type_doc
import ifcopenshell.util.date as dateutil
@@ -37,7 +36,6 @@ class SequenceData:
@classmethod
def load(cls):
cls.data = {
"predefined_types": cls.get_work_schedule_types(),
"has_work_plans": cls.has_work_plans(),
"has_work_schedules": cls.has_work_schedules(),
"has_work_calendars": cls.has_work_calendars(),
@@ -230,22 +228,6 @@ class SequenceData:
data["NestingIndex"] = rel.RelatedObjects.index(task)
cls.data["tasks"][task.id()] = data
@classmethod
def get_work_schedule_types(cls):
results = []
declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule")
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
results.extend(
[
(e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e))
for e in attribute.type_of_attribute().declared_type().enumeration_items()
]
)
break
return results
class WorkScheduleData:
data = {}
@@ -129,9 +129,21 @@ class AddWorkSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_work_schedule"
bl_label = "Add Work Schedule"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
def _execute(self, context):
core.add_work_schedule(tool.Ifc)
core.add_work_schedule(tool.Ifc, tool.Sequence, name=self.name)
def draw(self, context):
layout = self.layout
layout.prop(self, "name", text="Name")
self.props = context.scene.BIMWorkScheduleProperties
layout.prop(self.props, "work_schedule_predefined_types", text="Type")
if self.props.work_schedule_predefined_types == "USERDEFINED":
layout.prop(self.props,"object_type", text="Object type")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class EditWorkSchedule(bpy.types.Operator, tool.Ifc.Operator):
@@ -1389,3 +1401,22 @@ class CreateBaseline(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class ClearPreviousAnimation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.clear_previous_animation"
bl_label = "Clear Previous Animation"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.clear_previous_animation(tool.Sequence)
class AddAnimationCamera(bpy.types.Operator):
bl_idname = "bim.add_animation_camera"
bl_label = "Add Camera to Scene"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.add_animation_camera(tool.Sequence)
return {"FINISHED"}
@@ -20,6 +20,7 @@ import bpy
import isodate
import ifcopenshell.api
import ifcopenshell.util.attribute
from ifcopenshell.util.doc import get_predefined_type_doc
import blenderbim.tool as tool
import blenderbim.core.sequence as core
from blenderbim.bim.ifc import IfcStore
@@ -208,26 +209,31 @@ def updateTaskDuration(self, context):
self.duration = "-"
return
self.file = tool.Ifc.get()
task = self.file.by_id(self.ifc_definition_id)
task = tool.Ifc.get().by_id(self.ifc_definition_id)
if task.TaskTime:
task_time = task.TaskTime
else:
task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=task)
ifcopenshell.api.run(
"sequence.edit_task_time",
self.file,
**{"task_time": task_time, "attributes": {"ScheduleDuration": duration}},
)
task_time = tool.Ifc.run("sequence.add_task_time", task=task)
tool.Ifc.run("sequence.edit_task_time", task_time=task_time, attributes={"ScheduleDuration": duration})
SequenceData.load()
bpy.ops.bim.load_task_properties()
def get_schedule_predefined_types(self, context):
if not SequenceData.is_loaded:
SequenceData.load()
return SequenceData.data["predefined_types"]
results = []
declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule")
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
results.extend(
[
(e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e))
for e in attribute.type_of_attribute().declared_type().enumeration_items()
if e != "BASELINE"
]
)
break
return results
def update_visualisation_start(self, context):
update_visualisation_start_finish(self, context, "visualisation_start")
@@ -292,6 +298,14 @@ def update_filter_by_active_schedule(self, context):
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
def switch_options(self, context):
if self.should_show_visualisation_ui:
self.should_show_snapshot_ui = False
def switch_options2(self, context):
if self.should_show_snapshot_ui:
self.should_show_visualisation_ui = False
class Task(PropertyGroup):
name: StringProperty(name="Name", update=updateTaskName)
identification: StringProperty(name="Identification", update=updateTaskIdentification)
@@ -352,6 +366,7 @@ class BIMWorkScheduleProperties(PropertyGroup):
work_schedule_predefined_types: EnumProperty(
items=get_schedule_predefined_types, name="Predefined Type", default=None
)
object_type: StringProperty(name="Object Type")
durations_attributes: CollectionProperty(name="Durations Attributes", type=ISODuration)
work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars")
work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute)
@@ -362,9 +377,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_index: IntProperty(name="Active Task Index", update=update_active_task_index)
active_task_id: IntProperty(name="Active Task Id")
task_attributes: CollectionProperty(name="Task Attributes", type=Attribute)
should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False)
should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=True, update=switch_options)
should_show_task_bar_selection: BoolProperty(name="Add to task bar", default=False)
should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False)
should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False, update=switch_options2)
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
columns: CollectionProperty(name="Columns", type=Attribute)
active_column_index: IntProperty(name="Active Column Index")
@@ -397,9 +412,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
visualisation_start: StringProperty(name="Visualisation Start", update=update_visualisation_start)
visualisation_finish: StringProperty(name="Visualisation Finish", update=update_visualisation_finish)
speed_multiplier: FloatProperty(name="Speed Multiplier", default=10000)
speed_animation_duration: StringProperty(name="Speed Animation Duration", default="PT1S")
speed_animation_duration: StringProperty(name="Speed Animation Duration", default="1 s")
speed_animation_frames: IntProperty(name="Speed Animation Frames", default=24)
speed_real_duration: StringProperty(name="Speed Real Duration", default="P1W")
speed_real_duration: StringProperty(name="Speed Real Duration", default="1 w")
speed_types: EnumProperty(
items=[
("FRAME_SPEED", "Frame-based", "e.g. 25 frames = 1 real week"),
@@ -22,7 +22,6 @@ from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from blenderbim.bim.module.sequence.data import WorkPlansData, WorkScheduleData, SequenceData, TaskICOMData
import blenderbim.tool as tool
class BIM_PT_work_plans(Panel):
@@ -32,10 +31,12 @@ class BIM_PT_work_plans(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_4D5D"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3"
file = IfcStore.get_file()
return file and file.schema != "IFC2X3"
def draw(self, context):
if not WorkPlansData.is_loaded:
@@ -54,11 +55,10 @@ class BIM_PT_work_plans(Panel):
def draw_work_plan_ui(self, work_plan):
row = self.layout.row(align=True)
row.label(text=work_plan["name"], icon="TEXT")
if self.props.active_work_plan_id == work_plan["id"]:
if self.props.editing_type == "ATTRIBUTES":
row.operator("bim.edit_work_plan", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_work_plan", text="", icon="CANCEL")
row.operator("bim.disable_editing_work_plan", text="Cancel", icon="CANCEL")
elif self.props.active_work_plan_id:
row.operator("bim.remove_work_plan", text="", icon="X").work_plan = work_plan["id"]
else:
@@ -101,10 +101,12 @@ class BIM_PT_work_schedules(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_4D5D"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3"
file = IfcStore.get_file()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
if not SequenceData.is_loaded:
@@ -113,10 +115,9 @@ class BIM_PT_work_schedules(Panel):
WorkScheduleData.load()
self.props = context.scene.BIMWorkScheduleProperties
self.tprops = context.scene.BIMTaskTreeProperties
self.animation_props = context.scene.BIMAnimationProperties
if not self.props.active_work_schedule_id:
row = self.layout.row()
row = self.layout.row(align=True)
if SequenceData.data["has_work_schedules"]:
row.label(
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
@@ -124,25 +125,25 @@ class BIM_PT_work_schedules(Panel):
)
else:
row.label(text="No Work Schedules found.", icon="TEXT")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "work_schedule_predefined_types")
row.operator("bim.add_work_schedule", text="Add", icon="ADD")
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
if not work_schedule["PredefinedType"] == "BASELINE":
if work_schedule["PredefinedType"] == "BASELINE":
self.draw_readonly_work_schedule_ui(work_schedule_id)
else:
row = self.layout.row(align=True)
row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
if self.props.active_work_schedule_id == work_schedule_id:
row.label(
text="Currently editing: {}[{}]".format(work_schedule["Name"], work_schedule["PredefinedType"]),
icon="LINENUMBERS_ON",
)
if self.props.editing_type == "WORK_SCHEDULE":
row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK")
row.operator("bim.edit_work_schedule", text="Apply", icon="CHECKMARK")
elif self.props.editing_type == "TASKS":
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
row1.alignment = "LEFT"
@@ -177,33 +178,23 @@ class BIM_PT_work_schedules(Panel):
row1.alignment = "RIGHT"
row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY")
row2 = col.row(align=True)
row2.prop(
self.props, "should_show_visualisation_ui", text="Animation Options", icon="CAMERA_STEREO"
)
row2.prop(self.props, "should_show_snapshot_ui", text="Snapshot Options", icon="CAMERA_STEREO")
row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL")
else:
row.operator("bim.disable_editing_work_schedule", text="Cancel", icon="CANCEL")
if not self.props.active_work_schedule_id:
row.label(text="{}[{}]".format(work_schedule["Name"], work_schedule["PredefinedType"]) or "Unnamed", icon="LINENUMBERS_ON")
row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
"bim.enable_editing_work_schedule_tasks", text="Tasks", icon="ACTION"
).work_schedule = work_schedule_id
row.operator(
"bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL"
"bim.enable_editing_work_schedule", text="Attributes", icon="GREASEPENCIL"
).work_schedule = work_schedule_id
row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
row.operator("bim.remove_work_schedule", text="Delete", icon="X").work_schedule = work_schedule_id
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
self.draw_editable_work_schedule_ui()
elif self.props.editing_type == "TASKS":
self.draw_baseline_ui(work_schedule_id)
self.draw_column_ui()
if self.props.should_show_visualisation_ui:
self.draw_visualisation_ui()
if self.props.should_show_snapshot_ui:
self.draw_snapshot_ui()
self.draw_editable_task_ui(work_schedule_id)
else:
self.draw_readonly_work_schedule_ui(work_schedule_id)
def draw_task_operators(self):
row = self.layout.row(align=True)
@@ -218,7 +209,7 @@ class BIM_PT_work_schedules(Panel):
row.operator("bim.edit_task_time", text="", icon="CHECKMARK")
elif self.props.editing_task_type == "ATTRIBUTES":
row.operator("bim.edit_task", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_task", text="", icon="CANCEL")
row.operator("bim.disable_editing_task", text="Cancel", icon="CANCEL")
else:
row.prop(self.props, "show_task_operators", text="Edit", icon="GREASEPENCIL")
if self.props.show_task_operators:
@@ -242,7 +233,7 @@ class BIM_PT_work_schedules(Panel):
if not self.props.should_show_column_ui:
return
row = self.layout.row()
row.operator("bim.setup_default_task_columns", text="Add Default Columns", icon="ANCHOR_BOTTOM")
row.operator("bim.setup_default_task_columns", text="Setup Default Columns", icon="ANCHOR_BOTTOM")
row.alignment = "RIGHT"
row = self.layout.row(align=True)
row.prop(self.props, "column_types", text="")
@@ -268,103 +259,6 @@ class BIM_PT_work_schedules(Panel):
self.layout.template_list("BIM_UL_task_columns", "", self.props, "columns", self.props, "active_column_index")
def draw_visualisation_ui(self):
row = self.layout.row(align=True)
row.label(text="Start Date/ Date Range:")
row = self.layout.row(align=True)
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW")
op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
op = row.operator("bim.datepicker", text=self.props.visualisation_finish or "Finish Date", icon="FF")
op.target_prop = "BIMWorkScheduleProperties.visualisation_finish"
op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH")
op.work_schedule = self.props.active_work_schedule_id
row = self.layout.row(align=True)
row.label(text="Speed Settings")
row = self.layout.row(align=True)
row.prop(self.props, "speed_types", text="")
if self.props.speed_types == "FRAME_SPEED":
row.prop(self.props, "speed_animation_frames", text="")
row.prop(self.props, "speed_real_duration", text="")
elif self.props.speed_types == "DURATION_SPEED":
row.prop(self.props, "speed_animation_duration", text="")
row.prop(self.props, "speed_real_duration", text="")
elif self.props.speed_types == "MULTIPLIER_SPEED":
row.prop(self.props, "speed_multiplier", text="")
row = self.layout.row(align=True)
row.label(text="Display Settings")
row = self.layout.row(align=True)
if not self.animation_props.is_editing:
op = row.operator(
"bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04"
)
else:
op = row.operator(
"bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01"
)
row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN")
if self.animation_props.should_show_task_bar_options:
row = self.layout.row()
row.label(text="Task Bar Options", icon="NLA_PUSHDOWN")
row.alignment = "LEFT"
row = self.layout.row(align=True)
row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN")
row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN")
grid = self.layout.grid_flow(columns=2, even_columns=True)
# Column1
col = grid.column()
row = col.row(align=True)
row.prop(self.animation_props, "color_progress")
row = col.row(align=True)
row.prop(self.animation_props, "color_full")
if self.animation_props.is_editing:
self.draw_visualisation_settings_ui()
row = self.layout.row(align=True)
op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA")
op.work_schedule = self.props.active_work_schedule_id
def draw_snapshot_ui(self):
row = self.layout.row(align=True)
row.label(text="Create Construction Snapshot:")
row = self.layout.row(align=True)
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="REW")
op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="RESTRICT_RENDER_OFF")
op.work_schedule = self.props.active_work_schedule_id
def draw_visualisation_settings_ui(self):
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
row1.label(text="INPUT COLORS", icon="COLLECTION_COLOR_01")
row1 = col.row()
row1.template_list(
"BIM_UL_animation_colors",
"",
self.animation_props,
"task_colors_components_inputs",
self.animation_props,
"active_color_component_inputs_index",
)
col = grid.column()
row1 = col.row(align=True)
row1.label(text="OUTPUT COLORS", icon="COLLECTION_COLOR_04")
row1 = col.row()
row1.template_list(
"BIM_UL_animation_colors",
"",
self.animation_props,
"task_colors_components_outputs",
self.animation_props,
"active_color_component_outputs_index",
)
def draw_editable_work_schedule_ui(self):
draw_attributes(self.props.work_schedule_attributes, self.layout)
@@ -421,12 +315,12 @@ class BIM_PT_work_schedules(Panel):
if self.props.active_sequence_id == sequence["id"]:
if self.props.editing_sequence_type == "ATTRIBUTES":
row.operator("bim.edit_sequence_attributes", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_sequence", text="", icon="CANCEL")
row.operator("bim.disable_editing_sequence", text="Cancel", icon="CANCEL")
self.draw_editable_sequence_attributes_ui()
elif self.props.editing_sequence_type == "LAG_TIME":
op = row.operator("bim.edit_sequence_lag_time", text="", icon="CHECKMARK")
op.lag_time = sequence["TimeLag"]
row.operator("bim.disable_editing_sequence", text="", icon="CANCEL")
row.operator("bim.disable_editing_sequence", text="Cancel", icon="CANCEL")
self.draw_editable_sequence_lag_time_ui()
else:
if sequence["TimeLag"]:
@@ -491,7 +385,7 @@ class BIM_PT_work_schedules(Panel):
"id"
]
baseline_row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
"bim.enable_editing_work_schedule_tasks", text="Display Schedule", icon="ACTION"
).work_schedule = baseline["id"]
baseline_row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = baseline["id"]
@@ -521,6 +415,151 @@ class BIM_PT_work_schedules(Panel):
)
class BIM_PT_animation_tools(Panel):
bl_label = "Animation Tools"
bl_idname = "BIM_PT_animation_tools"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_work_schedules"
@classmethod
def poll(cls, context):
props = context.scene.BIMWorkScheduleProperties
if props.active_work_schedule_id:
return True
return False
def draw(self, context):
self.props = context.scene.BIMWorkScheduleProperties
self.animation_props = context.scene.BIMAnimationProperties
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(
self.props, "should_show_visualisation_ui", text="Animation Settings", icon="SETTINGS"
)
row.prop(self.props, "should_show_snapshot_ui", text="Snapshot Settings", icon="SETTINGS")
if self.props.should_show_visualisation_ui:
self.draw_visualisation_ui()
if self.props.should_show_snapshot_ui:
self.draw_snapshot_ui()
self.draw_processing_options()
def draw_processing_options(self):
row = self.layout.row(align=True)
row.alignment = "LEFT"
row.label(text="Processing Tools")
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.clear_previous_animation", text="Clear Previous Animation", icon="TRACKING_CLEAR_FORWARDS")
row.operator("bim.add_animation_camera", text="Add Camera", icon="CAMERA_DATA")
def draw_visualisation_ui(self):
row = self.layout.row(align=True)
row.label(text="Start Date/ Date Range:", icon="CAMERA_DATA")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW")
op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
op = row.operator("bim.datepicker", text=self.props.visualisation_finish or "Finish Date", icon="FF")
op.target_prop = "BIMWorkScheduleProperties.visualisation_finish"
op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH")
op.work_schedule = self.props.active_work_schedule_id
row = self.layout.row(align=True)
row.label(text="Speed Settings")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "speed_types", text="")
if self.props.speed_types == "FRAME_SPEED":
row.prop(self.props, "speed_animation_frames", text="")
row.prop(self.props, "speed_real_duration", text="")
elif self.props.speed_types == "DURATION_SPEED":
row.prop(self.props, "speed_animation_duration", text="")
row.label(text="->")
row.prop(self.props, "speed_real_duration", text="")
elif self.props.speed_types == "MULTIPLIER_SPEED":
row.prop(self.props, "speed_multiplier", text="")
row = self.layout.row(align=True)
row.label(text="Display Settings")
row = self.layout.row()
row.alignment = "RIGHT"
if not self.animation_props.is_editing:
op = row.operator(
"bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04"
)
else:
op = row.operator(
"bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01"
)
row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN")
if self.animation_props.should_show_task_bar_options:
row = self.layout.row()
row.label(text="Task Bar Options", icon="NLA_PUSHDOWN")
row.alignment = "LEFT"
row = self.layout.row(align=True)
row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN")
row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN")
grid = self.layout.grid_flow(columns=2, even_columns=True)
# Column1
col = grid.column()
row = col.row(align=True)
row.prop(self.animation_props, "color_progress")
row = col.row(align=True)
row.prop(self.animation_props, "color_full")
if self.animation_props.is_editing:
self.draw_visualisation_settings_ui()
row = self.layout.row(align=True)
op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA")
op.work_schedule = self.props.active_work_schedule_id
def draw_snapshot_ui(self):
row = self.layout.row(align=True)
row.label(text="Date of Snapshot:", icon="CAMERA_STEREO")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="PROP_PROJECTED")
op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
row = self.layout.row(align=True)
row.alignment = "RIGHT"
op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="CAMERA_STEREO")
op.work_schedule = self.props.active_work_schedule_id
def draw_visualisation_settings_ui(self):
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
row1.label(text="INPUT COLORS", icon="COLLECTION_COLOR_01")
row1 = col.row()
row1.template_list(
"BIM_UL_animation_colors",
"",
self.animation_props,
"task_colors_components_inputs",
self.animation_props,
"active_color_component_inputs_index",
)
col = grid.column()
row1 = col.row(align=True)
row1.label(text="OUTPUT COLORS", icon="COLLECTION_COLOR_04")
row1 = col.row()
row1.template_list(
"BIM_UL_animation_colors",
"",
self.animation_props,
"task_colors_components_outputs",
self.animation_props,
"active_color_component_outputs_index",
)
class BIM_PT_task_icom(Panel):
bl_label = "Task ICOM"
bl_idname = "BIM_PT_task_icom"
@@ -813,10 +852,12 @@ class BIM_PT_work_calendars(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_4D5D"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3"
file = IfcStore.get_file()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
if not SequenceData.is_loaded:
@@ -841,7 +882,7 @@ class BIM_PT_work_calendars(Panel):
if self.props.active_work_calendar_id == work_calendar_id:
if self.props.editing_type == "ATTRIBUTES":
row.operator("bim.edit_work_calendar", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_work_calendar", text="", icon="CANCEL")
row.operator("bim.disable_editing_work_calendar", text="Cancel", icon="CANCEL")
elif self.props.active_work_calendar_id:
row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = work_calendar_id
else:
@@ -879,7 +920,7 @@ class BIM_PT_work_calendars(Panel):
row.label(text="{} - {}".format(work_time["Start"] or "*", work_time["Finish"] or "*"))
if self.props.active_work_time_id == work_time["id"]:
row.operator("bim.edit_work_time", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_work_time", text="", icon="CANCEL")
row.operator("bim.disable_editing_work_time", text="Cancel", icon="CANCEL")
elif self.props.active_work_time_id:
op = row.operator("bim.remove_work_time", text="", icon="X")
op.work_time = work_time["id"]
@@ -34,6 +34,7 @@ classes = (
operator.RemovePort,
operator.RemoveSystem,
operator.SelectSystemProducts,
operator.MEPConnectElements,
operator.SetFlowDirection,
operator.ShowPorts,
operator.UnassignSystem,
@@ -91,8 +91,15 @@ class PortData:
@classmethod
def load(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
cls.element = element
is_port = cls.is_port()
cls.data = {
"total_ports": cls.total_ports(),
"located_ports_data": cls.located_ports_data(),
"is_port": is_port,
"port_connected_object": cls.port_connected_object() if is_port else None,
"port_relating_object": cls.port_relating_object() if is_port else None,
}
cls.is_loaded = True
@@ -100,3 +107,36 @@ class PortData:
def total_ports(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
return len(ifcopenshell.util.system.get_ports(element))
@classmethod
def is_port(cls):
return cls.element and cls.element.is_a("IfcDistributionPort")
@classmethod
def port_relating_object(cls):
return tool.Ifc.get_object(tool.System.get_port_relating_element(cls.element))
@classmethod
def port_connected_object(cls):
connected_port = tool.System.get_connected_port(cls.element)
if not connected_port:
return
connected_element = tool.System.get_port_relating_element(connected_port)
return tool.Ifc.get_object(connected_element)
@classmethod
def located_ports_data(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
ports = ifcopenshell.util.system.get_ports(element)
data = []
for port in ports:
port_obj = tool.Ifc.get_object(port)
connected_port = tool.System.get_connected_port(port)
if connected_port:
connected_element = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port))
else:
connected_element = None
data.append((port, port_obj, connected_element))
return data
@@ -23,6 +23,7 @@ import blenderbim.core.system as core
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.system.data import PortData
from mathutils import Matrix
class Operator:
@@ -205,8 +206,50 @@ class DisconnectPort(bpy.types.Operator, Operator):
bl_label = "Disconnect Ports"
bl_options = {"REGISTER", "UNDO"}
element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"})
def _execute(self, context):
core.disconnect_port(tool.Ifc, port=tool.Ifc.get_entity(context.active_object))
if self.element_id != 0:
element = tool.Ifc.get().by_id(self.element_id)
else:
element = tool.Ifc.get_entity(context.active_object)
core.disconnect_port(tool.Ifc, port=element)
class MEPConnectElements(bpy.types.Operator, Operator):
bl_idname = "bim.mep_connect_elements"
bl_label = "Connect MEP Elements"
bl_description = "Connects two selected elements if they have ports with matching location"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) == 2
def _execute(self, context):
obj1 = context.active_object
obj2 = next(o for o in context.selected_objects if o != obj1)
el1 = tool.Ifc.get_entity(obj1)
el2 = tool.Ifc.get_entity(obj2)
obj1_ports = [p for p in tool.System.get_ports(el1) if not tool.System.get_connected_port(p)]
obj2_ports = [p for p in tool.System.get_ports(el2) if not tool.System.get_connected_port(p)]
if not obj1_ports or not obj2_ports:
self.report({"ERROR"}, "Couldn't find free ports to connect.")
return
for port1 in obj1_ports:
port1_location = tool.Model.get_element_matrix(port1).translation
for port2 in obj2_ports:
port2_location = tool.Model.get_element_matrix(port2).translation
if tool.Cad.are_vectors_equal(port1_location, port2_location):
core.connect_port(tool.Ifc, port1, port2)
return {"FINISHED"}
self.report({"ERROR"}, "Couldn't find any matching ports to connect.")
return {"CANCELLED"}
class SetFlowDirection(bpy.types.Operator, Operator):
@@ -23,6 +23,14 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, PortData
FLOW_DIRECTION_TO_ICON = {
"SOURCE": "FORWARD",
"SINK": "BACK",
"SOURCEANDSINK": "ARROW_LEFTRIGHT",
"NOTDEFINED": "RESTRICT_INSTANCED_ON",
}
class BIM_PT_systems(Panel):
bl_label = "Systems"
bl_idname = "BIM_PT_systems"
@@ -115,6 +123,7 @@ class BIM_PT_object_systems(Panel):
"IfcDistributionSystem": "NETWORK_DRIVE",
"IfcDistributionCircuit": "DRIVER",
"IfcBuildingSystem": "MOD_BUILD",
"IfcBuiltSystem": "MOD_BUILD",
"IfcZone": "CUBE",
}
for system in ObjectSystemData.data["systems"]:
@@ -156,11 +165,44 @@ class BIM_PT_ports(Panel):
self.props = context.scene.BIMSystemProperties
row = self.layout.row(align=True)
row.label(text=f"{PortData.data['total_ports']} Ports Found", icon="PLUGIN")
total_ports = PortData.data["total_ports"]
row.label(text=f"{total_ports} Ports Found", icon="PLUGIN")
row.operator("bim.mep_connect_elements", text="", icon="PLUGIN")
row.operator("bim.show_ports", icon="HIDE_OFF", text="")
row.operator("bim.hide_ports", icon="HIDE_ON", text="")
row.operator("bim.add_port", icon="ADD", text="")
if total_ports == 0:
return
row = self.layout.row(align=True)
row.label(text="Ports located on object and connected objects:")
row = self.layout.row(align=True)
cols = [row.column(align=True) for i in range(6)]
for i, port_data in enumerate(PortData.data["located_ports_data"]):
port, port_obj, connected_obj = port_data
flow_direction_icon = FLOW_DIRECTION_TO_ICON[port.FlowDirection or "NOTDEFINED"]
if port_obj:
cols[0].label(text="", icon=flow_direction_icon)
cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port.id()
cols[2].label(text=port_obj.name)
else:
cols[0].label(text="", icon=flow_direction_icon)
cols[1].label(text="", icon="HIDE_ON")
cols[2].label(text="Port is hidden")
if connected_obj:
cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id()
cols[4].operator(
"bim.select_entity", text="", icon="RESTRICT_SELECT_OFF"
).ifc_id = connected_obj.BIMObjectProperties.ifc_definition_id
cols[5].label(text=f"{connected_obj.name}")
else:
cols[3].label(text="", icon="UNLINKED")
cols[4].label(text="", icon="BLANK1")
cols[5].label(text="Port is disconnected")
class BIM_PT_port(Panel):
bl_label = "Port"
@@ -184,36 +226,56 @@ class BIM_PT_port(Panel):
def draw(self, context):
self.props = context.scene.BIMSystemProperties
element = tool.Ifc.get_entity(context.active_object)
port_class = element.is_a()
layout = self.layout
row = layout.row(align=True)
row.label(text=port_class)
row.label(text="IfcDistributionPort")
row.operator("bim.connect_port", icon="PLUGIN", text="")
row.operator("bim.disconnect_port", icon="UNLINKED", text="")
row.operator("bim.remove_port", icon="X", text="")
if port_class == "IfcDistributionPort":
current_flow_direction = str(element.FlowDirection)
row = layout.row(align=True)
row.label(text="Flow Direction:")
row.label(text=current_flow_direction)
if not PortData.is_loaded:
PortData.load()
# TODO: replace with enum property?
flow_directions = (
("SOURCE", "FORWARD"),
("SINK", "BACK"),
("SOURCEANDSINK", "ARROW_LEFTRIGHT"),
("NOTDEFINED", "RESTRICT_INSTANCED_ON"),
)
if not PortData.data["is_port"]:
return
row = layout.row(align=True)
row.label(text="Change Flow Direction:")
for flow_direction, icon in flow_directions:
row = layout.row()
row.operator("bim.set_flow_direction", icon=icon, text=flow_direction).direction = flow_direction
if flow_direction == current_flow_direction:
row.enabled = False
element = tool.Ifc.get_entity(context.active_object)
current_flow_direction = str(element.FlowDirection)
row = layout.row(align=True)
row.label(text="Flow Direction:")
row.label(text=current_flow_direction)
# port located on
row = layout.row(align=True)
relating_object = PortData.data["port_relating_object"]
row.label(text="Port located on:")
row.label(text=relating_object.name)
row.operator(
"bim.select_entity", text="", icon="RESTRICT_SELECT_OFF"
).ifc_id = relating_object.BIMObjectProperties.ifc_definition_id
# object connected to the port
row = layout.row(align=True)
connected_object = PortData.data["port_connected_object"]
if connected_object:
row.label(text="Port connected to:")
row.label(text=connected_object.name)
row.operator(
"bim.select_entity", text="", icon="RESTRICT_SELECT_OFF"
).ifc_id = connected_object.BIMObjectProperties.ifc_definition_id
else:
row.label(text="Port is not connected to any element")
# TODO: replace with enum property?
row = layout.row(align=True)
row.label(text="Change Flow Direction:")
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
row = layout.row()
row.operator(
"bim.set_flow_direction", icon=FLOW_DIRECTION_TO_ICON[flow_direction], text=flow_direction
).direction = flow_direction
if flow_direction == current_flow_direction:
row.enabled = False
class BIM_UL_systems(UIList):
@@ -223,6 +285,7 @@ class BIM_UL_systems(UIList):
"IfcDistributionSystem": "NETWORK_DRIVE",
"IfcDistributionCircuit": "DRIVER",
"IfcBuildingSystem": "MOD_BUILD",
"IfcBuiltSystem": "MOD_BUILD",
"IfcZone": "CUBE",
}
if item:
@@ -255,6 +318,7 @@ class BIM_UL_object_systems(UIList):
"IfcDistributionSystem": "NETWORK_DRIVE",
"IfcDistributionCircuit": "DRIVER",
"IfcBuildingSystem": "MOD_BUILD",
"IfcBuiltSystem": "MOD_BUILD",
"IfcZone": "CUBE",
}
if item:
@@ -312,32 +312,36 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
)
else:
# NOTE: defaults dims are in meters / mm
# for now default names are hardcoded to mm
if template == "FLOW_SEGMENT_RECTANGULAR":
default_x_dim = 0.4 / unit_scale
default_y_dim = 0.2 / unit_scale
profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_x_dim*1000}"
default_x_dim = 0.4
default_y_dim = 0.2
profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_y_dim*1000}"
profile = ifc_file.create_entity(
"IfcRectangleProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
XDim=default_x_dim,
YDim=default_y_dim,
XDim=default_x_dim / unit_scale,
YDim=default_y_dim / unit_scale,
)
elif template == "FLOW_SEGMENT_CIRCULAR":
default_diameter = 0.1 / unit_scale
default_diameter = 0.1
profile_name = f"{ifc_class}-{default_diameter*1000}"
profile = ifc_file.create_entity(
"IfcCircleProfileDef", ProfileName=profile_name, ProfileType="AREA", Radius=default_diameter / 2
"IfcCircleProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
Radius=(default_diameter / 2) / unit_scale,
)
elif template == "FLOW_SEGMENT_CIRCULAR_HOLLOW":
default_diameter = 0.15 / unit_scale
default_thickness = 0.005 / unit_scale
default_diameter = 0.15
default_thickness = 0.005
profile_name = f"{ifc_class}-{default_diameter*1000}x{default_thickness*1000}"
profile = ifc_file.create_entity(
"IfcCircleHollowProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
Radius=default_diameter / 2,
Radius=(default_diameter / 2) / unit_scale,
WallThickness=default_thickness,
)
+1 -1
View File
@@ -305,7 +305,7 @@ def get_tab(self, context):
("DRAWINGS", "Drawings and Documents", "", "DOCUMENTS", 3),
("SERVICES", "Services and Systems", "", "NETWORK_DRIVE", 4),
("STRUCTURE", "Structural Analysis", "", "EDITMODE_HLT", 5),
("SCHEDULING", "Construction Scheduling", "", "NLA", 6),
("SCHEDULING", "Costing and Scheduling", "", "NLA", 6),
("FM", "Facility Management", "", "PACKAGE", 7),
("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8),
("BLENDER", "Blender Properties", "", "BLENDER", 9),
+15
View File
@@ -404,6 +404,21 @@ class BIM_PT_geometry(Panel):
pass
class BIM_PT_tab_4D5D(Panel):
bl_label = "Costing and Scheduling"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get()
def draw(self, context):
pass
class BIM_PT_tab_structural(Panel):
bl_label = "Structural"
bl_space_type = "PROPERTIES"
+3 -3
View File
@@ -51,6 +51,8 @@ def close_brick_project(brick):
brick.clear_project()
brick.clear_brick_browser()
brick.clear_brick_browser(split_screen=True)
brick.clear_breadcrumbs()
brick.clear_breadcrumbs(split_screen=True)
def convert_brick_project(ifc, brick):
@@ -132,9 +134,7 @@ def add_namespace(brick, alias=None, uri=None):
def set_brick_list_root(brick, brick_root=None, split_screen=False):
brick.clear_brick_browser(split_screen=split_screen)
brick.import_brick_classes(brick_root, split_screen=split_screen)
brick.set_active_brick_class(brick_root, split_screen=split_screen)
brick.run_view_brick_class(brick_class=brick_root, split_screen=split_screen)
brick.clear_breadcrumbs(split_screen=split_screen)
+3 -4
View File
@@ -345,11 +345,10 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None):
context = drawing_tool.get_annotation_context(
target_view := drawing_tool.get_drawing_target_view(drawing), object_type
)
target_view = drawing_tool.get_drawing_target_view(drawing)
context = drawing_tool.get_annotation_context(target_view, object_type)
if not context:
return f"No annotation context Annotation/{target_view} for drawing"
context = drawing_tool.create_annotation_context(target_view, object_type)
drawing_tool.show_decorations()
obj = drawing_tool.create_annotation_object(drawing, object_type)
@@ -83,4 +83,7 @@ def convert_global_to_local(georeference):
def convert_angle_to_coord(georeference, type):
vector_coordinates = georeference.angle2coords(georeference.get_angle(type), type)
georeference.set_vector_coordinates(vector_coordinates,type)
georeference.set_vector_coordinates(vector_coordinates,type)
def import_plot(georeference, filepath):
georeference.import_plot(filepath, georeference.get_map_conversion())
+10 -2
View File
@@ -50,8 +50,9 @@ def enable_editing_work_plan_schedules(sequence, work_plan=None):
sequence.enable_editing_work_plan_schedules(work_plan)
def add_work_schedule(ifc):
return ifc.run("sequence.add_work_schedule")
def add_work_schedule(ifc, sequence, name=None):
predefined_type, object_type = sequence.get_user_predefined_type()
return ifc.run("sequence.add_work_schedule", name=name, predefined_type=predefined_type, object_type=object_type)
def remove_work_schedule(ifc, work_schedule=None):
@@ -529,6 +530,7 @@ def disable_editing_task_animation_colors(sequence):
def visualise_work_schedule_date_range(sequence, work_schedule=None):
sequence.clear_objects_animation(include_blender_objects=False)
settings = sequence.get_animation_settings()
if settings:
product_frames = sequence.get_animation_product_frames(work_schedule, settings)
@@ -579,3 +581,9 @@ def reorder_task_nesting(ifc, sequence, task, new_index):
def create_baseline(ifc, sequence, work_schedule, name):
ifc.run("sequence.create_baseline", work_schedule=work_schedule, name=name)
def clear_previous_animation(sequence):
sequence.clear_objects_animation(include_blender_objects=False)
def add_animation_camera(sequence):
sequence.add_animation_camera()
+4 -1
View File
@@ -183,7 +183,6 @@ class Cost:
def get_cost_value_unit_component(cls): pass
def get_direct_cost_item_products(cls): pass
def get_highlighted_cost_item(cls): pass
def get_highlighted_cost_item(cls): pass
def get_products(cls, related_object_type): pass
def get_schedule_cost_items(cls, cost_schedule): pass
def get_units(cls): pass
@@ -648,6 +647,7 @@ class Search:
@interface
class Sequence:
def add_animation_camera(cls): pass
def add_task_column(cls, column_type, name, data_type): pass
def add_text_animation_handler(cls, settings): pass
def animate_consumption(cls, obj, start_frame, product_frame, color, animation_type): pass
@@ -660,6 +660,7 @@ class Sequence:
def animate_operation(cls, obj, start_frame, product_frame, color): pass
def animate_output(cls, obj, start_frame, product_frame): pass
def clear_object_animation(cls, obj): pass
def clear_object_color(cls, obj): pass
def clear_objects_animation(cls, include_blender_objects): pass
def contract_all_tasks(cls): pass
def contract_task(cls, task): pass
@@ -676,6 +677,7 @@ class Sequence:
def disable_editing_work_time(cls): pass
def disable_selecting_deleted_task(cls): pass
def disable_work_schedule(cls): pass
def display_object(cls, obj): pass
def enable_editing_rel_sequence_attributes(cls, rel_sequence): pass
def enable_editing_sequence_lag_time(cls, rel_sequence): pass
def enable_editing_task_animation_colors(cls): pass
@@ -719,6 +721,7 @@ class Sequence:
def get_task_time_attributes(cls): pass
def get_task_time(cls, task): pass
def get_tasks_for_product(cls, product, work_schedule): pass
def get_user_predefined_type(cls): pass
def get_work_calendar_attributes(cls): pass
def get_work_plan_attributes(cls): pass
def get_work_schedule_attributes(cls): pass
+49 -11
View File
@@ -23,6 +23,7 @@ import ifcopenshell.util.brick
import blenderbim.core.tool
import blenderbim.tool as tool
from contextlib import contextmanager
import datetime
try:
import brickschema
@@ -308,6 +309,8 @@ class Brick(blenderbim.core.tool.Brick):
with BrickStore.graph.new_changeset("PROJECT") as cs:
cs.load_file(filepath)
BrickStore.path = filepath
cls.set_last_saved()
BrickStore.load_sub_roots()
BrickStore.load_namespaces()
BrickStore.load_entity_classes()
BrickStore.load_relationships()
@@ -321,6 +324,7 @@ class Brick(blenderbim.core.tool.Brick):
with BrickStore.graph.new_changeset("SCHEMA") as cs:
cs.load_file(BrickStore.schema)
BrickStore.graph.bind("digitaltwin", Namespace("https://example.org/digitaltwin#"))
BrickStore.load_sub_roots()
BrickStore.load_namespaces()
BrickStore.load_entity_classes()
BrickStore.load_relationships()
@@ -340,10 +344,9 @@ class Brick(blenderbim.core.tool.Brick):
@classmethod
def remove_brick(cls, brick_uri):
if BrickStore.graph.triples((URIRef(brick_uri), None, None)):
with BrickStore.new_changeset() as cs:
for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)):
cs.remove(triple)
with BrickStore.new_changeset() as cs:
for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)):
cs.remove(triple)
@classmethod
def run_assign_brick_reference(cls, element=None, library=None, brick_uri=None):
@@ -379,6 +382,7 @@ class Brick(blenderbim.core.tool.Brick):
@classmethod
def serialize_brick(cls):
BrickStore.get_project().serialize(destination=BrickStore.path, format="turtle")
cls.set_last_saved()
@classmethod
def add_namespace(cls, alias, uri):
@@ -392,22 +396,24 @@ class Brick(blenderbim.core.tool.Brick):
else:
bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear()
@classmethod
def set_last_saved(cls):
save = os.path.getmtime(BrickStore.path)
save = datetime.datetime.fromtimestamp(save)
BrickStore.last_saved = f"{save.year}-{save.month}-{save.day} {save.hour}:{save.minute}"
class BrickStore:
schema = None # this is now a os path
path = None # file path if the project was loaded in
graph = None # this is the VersionedGraphCollection with 2 arbitrarily named graphs: "schema" and "project"
# "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities
last_saved = None
history = []
future = []
current_changesets = 0
history_size = 64
namespaces = []
root_classes = ["Equipment",
"Electrical_Equipment", "Fire_Safety_Equipment", "HVAC_Equipment", "Lighting_Equipment", "Meter",
"Location",
"System",
"Point",
"Alarm", "Command", "Parameter", "Sensor", "Setpoint", "Status"]
root_classes = ["Equipment", "Location", "System", "Point"]
entity_classes = {}
relationships = []
@@ -416,7 +422,9 @@ class BrickStore:
BrickStore.schema = None
BrickStore.graph = None
BrickStore.path = None
BrickStore.last_saved = None
BrickStore.namespaces = []
BrickStore.root_classes = ["Equipment", "Location", "System", "Point"]
BrickStore.entity_classes = {}
BrickStore.relationships = []
@@ -424,10 +432,36 @@ class BrickStore:
def get_project(cls):
return BrickStore.graph.graph_at(graph="PROJECT")
@classmethod
def load_sub_roots(cls):
query = BrickStore.graph.query(
"""
PREFIX brick: <https://brickschema.org/schema/Brick#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?subRoot ?subClasses WHERE {
{
SELECT ?subRoot (COUNT(?subClass) as ?subClasses) WHERE {
{
?subRoot rdfs:subClassOf brick:Equipment .
} UNION {
?subRoot rdfs:subClassOf brick:Point .
}
?subClass rdfs:subClassOf* ?subRoot .
}
GROUP BY ?subRoot
}
FILTER(?subClasses > 3)
}
"""
)
for row in query:
sub_root = row.get("subRoot").toPython().split("#")[-1]
BrickStore.root_classes.append(sub_root)
@classmethod
def load_namespaces(cls):
BrickStore.namespaces = []
keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org"]
keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org", "usefulinc.com", "xmlns.com", "opengis.net"]
for alias, uri in BrickStore.graph.namespaces():
ignore_namespace = False
for keyword in keyword_filter:
@@ -444,8 +478,12 @@ class BrickStore:
"""
PREFIX brick: <https://brickschema.org/schema/Brick#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
SELECT ?class WHERE {
?class rdfs:subClassOf* brick:{root_class} .
FILTER NOT EXISTS {
?class owl:deprecated true .
}
}
""".replace(
"{root_class}", root_class
+48 -2
View File
@@ -91,10 +91,14 @@ class Cad:
tolerance = VTX_PRECISION
if isinstance(x, (list, tuple)):
for y in x:
if value > (y - tolerance) and value < (y + tolerance):
if (y + tolerance) > value > (y - tolerance):
return True
return False
return value > (x - tolerance) and value < (x + tolerance)
return (x + tolerance) > value > (x - tolerance)
@classmethod
def are_vectors_equal(cls, v1: Vector, v2: Vector):
return cls.is_x((v2 - v1).length, 0)
@classmethod
def intersect_edges(cls, edge1, edge2):
@@ -227,6 +231,48 @@ class Cad:
res = [cls.is_point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]]
return len([i for i in res if i])
@classmethod
def get_edge_direction(cls, edge):
return (edge[1] - edge[0]).normalized()
@classmethod
def are_edges_collinear(cls, edge1, edge2):
def is_point_on_line(p, edge):
a1, a2 = edge
# comparing slopes between PA1 and A2A1
# using cross multiplication to avoid division by zero
return cls.is_x((p.y - a1.y) * (a2.x - a1.x), (a2.y - a1.y) * (p.x - a1.x))
edge1_dir = edge1[1] - edge1[0]
edge2_dir = edge2[1] - edge2[0]
if cls.is_x(edge1_dir.cross(edge2_dir).length_squared, 0): # check they are parallel
if is_point_on_line(edge1[0], edge2) or is_point_on_line(edge1[1], edge2):
return True
return False
@classmethod
def closest_points(cls, edge1, edge2):
"""
closest end points between `edge1` and `edge2` assuming `edge1` and `edge2` are collinear.
< returns two points, first one belongs to `edge1` and second to `edge2`
"""
direction = (edge1[1] - edge1[0]).normalized()
# Project points onto the line to get scalar values along the direction
points1_values = [(p, p.dot(direction)) for p in edge1]
points2_values = [(p, p.dot(direction)) for p in edge2]
# Sort the projections for both edges
sorted_points1 = sorted(points1_values, key=lambda el: el[1])
sorted_points2 = sorted(points2_values, key=lambda el: el[1])
# The closest points will be the last point of the first edge and the first point of the second edge
return sorted_points1[-1][0], sorted_points2[0][0]
@classmethod
def find_intersecting_edges(cls, bm, pt, idx1, idx2):
"""
+19 -10
View File
@@ -96,11 +96,11 @@ class Cost(blenderbim.core.tool.Cost):
props.contracted_cost_items = json.dumps(cls.contracted_cost_items)
@classmethod
def contract_cost_item(cls, cost_item_id):
def contract_cost_item(cls, cost_item):
props = bpy.context.scene.BIMCostProperties
if not hasattr(cls, "contracted_cost_items"):
cls.contracted_cost_items = json.loads(props.contracted_cost_items)
cls.contracted_cost_items.append(cost_item_id)
cls.contracted_cost_items.append(cost_item.id())
props.contracted_cost_items = json.dumps(cls.contracted_cost_items)
@classmethod
@@ -156,14 +156,18 @@ class Cost(blenderbim.core.tool.Cost):
@classmethod
def get_highlighted_cost_item(cls):
props = bpy.context.scene.BIMCostProperties
if not props.active_cost_schedule_id:
return
if props.active_cost_item_index < len(props.cost_items):
return tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
return None
return
@classmethod
def load_cost_item_types(cls, cost_item=None):
if not cost_item:
return
cost_item = cls.get_highlighted_cost_item()
if not cost_item:
return
props = bpy.context.scene.BIMCostProperties
props.cost_item_type_products.clear()
# TODO implement process and resource types
@@ -220,7 +224,7 @@ class Cost(blenderbim.core.tool.Cost):
selected_quantitites = []
unit = ""
for quantities in ifcopenshell.util.element.get_psets(product, qtos_only=True).values():
for qto in (tool.Ifc.get().by_id(quantities["id"]).Quantities or []):
for qto in tool.Ifc.get().by_id(quantities["id"]).Quantities or []:
for quantity in cost_item.CostQuantities:
if quantity == qto:
selected_quantitites.append(quantity)
@@ -509,8 +513,9 @@ class Cost(blenderbim.core.tool.Cost):
import subprocess
import os
import sys
if filepath:
path=filepath
path = filepath
else:
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", "cost_schedules")
@@ -518,6 +523,7 @@ class Cost(blenderbim.core.tool.Cost):
os.makedirs(path)
if format == "CSV":
from ifc5d.ifc5Dspreadsheet import Ifc5DCsvWriter
writer = Ifc5DCsvWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule)
writer.write()
elif format == "ODS":
@@ -528,7 +534,7 @@ class Cost(blenderbim.core.tool.Cost):
elif format == "XLSX":
from ifc5d.ifc5Dspreadsheet import Ifc5DXlsxWriter
writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule )
writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule)
writer.write()
try:
if path:
@@ -539,7 +545,7 @@ class Cost(blenderbim.core.tool.Cost):
elif sys.platform == "linux":
subprocess.call(["xdg-open", path])
except:
return 'Could not open file location'
return "Could not open file location"
@classmethod
def get_units(cls):
@@ -566,7 +572,6 @@ class Cost(blenderbim.core.tool.Cost):
name = f"{unit.Prefix} {name}"
return f"{unit.UnitType} / {name}"
@classmethod
def get_cost_schedule(cls, cost_item):
for rel in cost_item.HasAssignments or []:
@@ -649,6 +654,8 @@ class Cost(blenderbim.core.tool.Cost):
@classmethod
def toggle_cost_item_parent_change(cls, cost_item=None):
if not cost_item:
return
props = bpy.context.scene.BIMCostProperties
if props.change_cost_item_parent:
props.active_cost_item_id = cost_item.id()
@@ -681,12 +688,14 @@ class Cost(blenderbim.core.tool.Cost):
if product:
cost_items = ifcopenshell.util.cost.get_cost_items_for_product(product)
if pset:
def get_products_from_pset(pset):
products = []
for rel in pset.DefinesOccurrence or []:
if rel.is_a("IfcRelDefinesByProperties"):
products.extend(rel.RelatedObjects)
return products
products = get_products_from_pset(pset)
for product in products or []:
cost_items.extend(ifcopenshell.util.cost.get_cost_items_for_product(product))
@@ -705,4 +714,4 @@ class Cost(blenderbim.core.tool.Cost):
currency = props.custom_currency
return {
"Currency": currency,
}
}
+32 -5
View File
@@ -122,11 +122,10 @@ class Drawing(blenderbim.core.tool.Drawing):
# place the arrow
# NOTE: may not work correctly in EDIT mode
bbox = tool.Blender.get_object_bounding_box(stair)
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
arrow.location = stair.matrix_world @ Vector(
(bbox["min_x"], (bbox["max_y"] - bbox["min_y"]) / 2, bbox["max_z"])
)
last_step_x = max(v.co.x for v in stair.data.vertices if float_is_zero(v.co.z - bbox["max_z"]))
last_step_x = max(v.co.x for v in stair.data.vertices if tool.Cad.is_x(v.co.z - bbox["max_z"], 0))
arrow.data.splines[0].points[0].co = Vector((0, 0, 0, 1))
arrow.data.splines[0].points[1].co = Vector((last_step_x, 0, 0, 1))
@@ -346,6 +345,27 @@ class Drawing(blenderbim.core.tool.Drawing):
literals.append(literal_data)
return literals
@classmethod
def create_annotation_context(cls, target_view, object_type=None):
# checking PLAN target view and annotation type that doesn't require 3d
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in (
"FALL",
"SECTION_LEVEL",
"PLAN_LEVEL",
):
parent = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan")
else:
parent = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model")
return ifcopenshell.api.run(
"context.add_context",
tool.Ifc.get(),
context_type=parent.ContextType,
context_identifier="Annotation",
target_view=target_view,
parent=parent,
)
@classmethod
def get_annotation_context(cls, target_view, object_type=None):
# checking PLAN target view and annotation type that doesn't require 3d
@@ -451,7 +471,10 @@ class Drawing(blenderbim.core.tool.Drawing):
elif target_view == "REFLECTED_PLAN_VIEW":
if location_hint:
z = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint)).matrix_world.translation.z
return mathutils.Matrix(((-1, 0, 0, x), (0, 1, 0, y), (0, 0, -1, z + 1.6), (0, 0, 0, 1)))
m = mathutils.Matrix()
m[2][2] = -1
m.translation = (x, y, z + 1.6)
return m
return mathutils.Matrix(((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1)))
elif target_view == "ELEVATION_VIEW":
if location_hint == "NORTH":
@@ -678,6 +701,10 @@ class Drawing(blenderbim.core.tool.Drawing):
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
obj.matrix_world = mat
if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW":
obj.matrix_world[1][1] *= -1
tool.Geometry.record_object_position(obj)
tool.Collector.assign(obj)
@@ -1484,8 +1511,8 @@ class Drawing(blenderbim.core.tool.Drawing):
else:
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
elements.update(annotations)
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
elements.update(annotations)
exclude = pset.get("Exclude", None)
if exclude:
+6 -1
View File
@@ -56,8 +56,13 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def clear_scale(cls, obj):
# Note that clearing scale has no impact on cameras.
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
if obj.data.users == 1:
if not obj.data:
obj.matrix_world[0][0] = 1
obj.matrix_world[1][1] = 1
obj.matrix_world[2][2] = 1
elif obj.data.users == 1:
context_override = {}
context_override["object"] = context_override["active_object"] = obj
context_override["selected_objects"] = context_override["selected_editable_objects"] = [obj]
@@ -296,3 +296,37 @@ class Georeference(blenderbim.core.tool.Georeference):
elif type == "rel_y":
bpy.context.scene.BIMGeoreferenceProperties.y_axis_abscissa_output = str(x)
bpy.context.scene.BIMGeoreferenceProperties.y_axis_ordinate_output = str(y)
@classmethod
def import_plot(cls, filepath, map_conversion):
import bmesh
def parse_csv(file_path):
import csv
with open(file_path, "r") as f:
reader = csv.reader(f) # Assuming tab-delimited CSV
rows = []
for row in reader:
if len(row) == 0:
continue
rows.append(row)
return rows
rows = parse_csv(filepath)
vertices = []
for row in rows:
coordinates = cls.enh2xyz([float(row[0]), float(row[1]), float(row[2])], map_conversion)
vertices.append(coordinates)
mesh = bpy.data.meshes.new("mesh")
obj = bpy.data.objects.new("Plot Line", mesh)
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
obj.data
bm = bmesh.new()
for vertex in vertices:
bm.verts.new(vertex)
bm.to_mesh(mesh)
bm.free()
+1 -1
View File
@@ -110,7 +110,7 @@ class Loader(blenderbim.core.tool.Loader):
"IfcNormalisedRatioMeasure"
):
diffuse_color_value = surface_style["DiffuseColour"].wrappedValue
diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColor"][:3]] + [1]
diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColour"][:3]] + [1]
surface_style["DiffuseColour"] = ("IfcNormalisedRatioMeasure", diffuse_color)
else:
surface_style["DiffuseColour"] = None
+13 -7
View File
@@ -358,7 +358,7 @@ class Model(blenderbim.core.tool.Model):
is_closed = False
if curve.Segments:
for segment in curve.Segments:
if len(segment[0]) == 3: # IfcArcIndex
if segment.is_a("IfcArcIndex"):
is_arc = True
local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
global_point = position @ Vector(local_point).to_3d()
@@ -368,12 +368,13 @@ class Model(blenderbim.core.tool.Model):
cls.vertices.append(global_point)
cls.arcs.append([len(cls.vertices) - 2, len(cls.vertices) - 1])
else:
local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
global_point = position @ Vector(local_point).to_3d()
cls.vertices.append(global_point)
if is_arc:
cls.arcs[-1].append(len(cls.vertices) - 1)
is_arc = False
for segment_index in segment[0][0:-1]:
local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment_index - 1])
global_point = position @ Vector(local_point).to_3d()
cls.vertices.append(global_point)
if is_arc:
cls.arcs[-1].append(len(cls.vertices) - 1)
is_arc = False
if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]:
is_closed = True
@@ -768,6 +769,11 @@ class Model(blenderbim.core.tool.Model):
return
tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True)
@classmethod
def get_element_matrix(cls, element):
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
return Matrix(placement)
@classmethod
def reload_body_representation(cls, obj_or_objects):
"""Update body representation including all decomposed objects"""
+57 -4
View File
@@ -23,6 +23,7 @@ import blenderbim.core.tool
import blenderbim.core.geometry
import blenderbim.tool as tool
from mathutils import Vector
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
class Root(blenderbim.core.tool.Root):
@@ -57,7 +58,10 @@ class Root(blenderbim.core.tool.Root):
if not source.Representation:
return
dest.Representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback
tool.Ifc.get(),
source.Representation,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
)
elif dest.is_a("IfcTypeProduct"):
if not source.RepresentationMaps:
@@ -140,9 +144,58 @@ class Root(blenderbim.core.tool.Root):
for i, new_subelement in enumerate(new_subelements):
new_element = new_elements[i]
if data["type"] == "fill":
obj1 = tool.Ifc.get_object(new_element)
obj2 = tool.Ifc.get_object(new_subelement)
bpy.ops.bim.add_filled_opening(voided_obj=obj1.name, filling_obj=obj2.name)
element = new_element
filling = new_subelement
voided_obj = tool.Ifc.get_object(new_element)
filling_obj = tool.Ifc.get_object(new_subelement)
existing_opening_occurrence = subelement.FillsVoids[0].RelatingOpeningElement
opening = ifcopenshell.api.run(
"root.copy_class", tool.Ifc.get(), product=existing_opening_occurrence
)
ifcopenshell.api.run(
"geometry.edit_object_placement",
tool.Ifc.get(),
product=opening,
matrix=ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement),
is_si=False,
)
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
representation = ifcopenshell.util.representation.resolve_representation(representation)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation",
tool.Ifc.get(),
product=opening,
representation=mapped_representation,
)
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element)
ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling)
voided_objs = [voided_obj]
# Openings affect all subelements of an aggregate
for subelement in ifcopenshell.util.element.get_decomposition(element):
subobj = tool.Ifc.get_object(subelement)
if subobj:
voided_objs.append(subobj)
for voided_obj in voided_objs:
if voided_obj.data:
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@classmethod
def run_geometry_add_representation(
+2 -1
View File
@@ -55,7 +55,8 @@ class Search(blenderbim.core.tool.Search):
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"location{comparison}{value}")
if not has_instance_or_entity_filter:
filter_group_query.insert(0, "IfcElement")
filter_group_query.insert(0, "IfcProduct")
filter_group_query.insert(0, "IfcTypeProduct")
query.append(", ".join(filter_group_query))
return " + ".join(query)
+53 -32
View File
@@ -19,7 +19,6 @@
import bpy
import re
import os
import isodate
import ifcopenshell
import ifcopenshell.util.sequence
import ifcopenshell.util.date
@@ -36,7 +35,6 @@ from datetime import datetime
import mathutils
import pystache
import webbrowser
from datetime import timedelta
class Sequence(blenderbim.core.tool.Sequence):
@@ -212,7 +210,9 @@ class Sequence(blenderbim.core.tool.Sequence):
item.calendar = ""
item.derived_calendar = calendar.Name or "Unnamed" if calendar else ""
if task.TaskTime and (task.TaskTime.ScheduleStart or task.TaskTime.ScheduleFinish or task.TaskTime.ScheduleDuration):
if task.TaskTime and (
task.TaskTime.ScheduleStart or task.TaskTime.ScheduleFinish or task.TaskTime.ScheduleDuration
):
task_time = task.TaskTime
item.start = (
canonicalise_time(ifcopenshell.util.date.ifc2datetime(task_time.ScheduleStart))
@@ -719,26 +719,12 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def setup_default_task_columns(cls):
items = [
{
"column_type": "IfcTaskTime",
"name": "ScheduleStart",
},
{
"column_type": "IfcTaskTime",
"name": "ScheduleFinish",
},
{
"column_type": "IfcTaskTime",
"name": "ScheduleDuration",
},
]
props = bpy.context.scene.BIMWorkScheduleProperties
props.columns.clear()
for item in items:
default_columns = ["ScheduleStart", "ScheduleFinish", "ScheduleDuration"]
for item in default_columns:
new = props.columns.add()
new.name = f"{item['column_type']}.{item['name']}"
new.name = f"IfcTaskTime.{item}"
new.data_type = "string"
@classmethod
@@ -811,10 +797,10 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def update_visualisation_date(cls, start_date, finish_date):
def canonicalise_time(time):
if not time:
return "-"
return time.strftime("%d/%m/%y")
if not (start_date and finish_date):
return
props = bpy.context.scene.BIMWorkScheduleProperties
props.visualisation_start = canonicalise_time(start_date)
props.visualisation_finish = canonicalise_time(finish_date)
@@ -1227,21 +1213,23 @@ class Sequence(blenderbim.core.tool.Sequence):
start,
finish,
props.speed_animation_frames,
isodate.parse_duration(props.speed_real_duration),
ifcopenshell.util.date.parse_duration(props.speed_real_duration),
)
elif props.speed_types == "DURATION_SPEED":
animation_duration = ifcopenshell.util.date.parse_duration(props.speed_animation_duration)
real_duration = ifcopenshell.util.date.parse_duration(props.speed_real_duration)
return calculate_using_duration(
start,
finish,
fps,
isodate.parse_duration(props.speed_animation_duration),
isodate.parse_duration(props.speed_real_duration),
animation_duration,
real_duration,
)
elif props.speed_types == "MULTIPLIER_SPEED":
return calculate_using_multiplier(
start,
finish,
fps,
1,
props.speed_multiplier,
)
@@ -1312,15 +1300,24 @@ class Sequence(blenderbim.core.tool.Sequence):
if obj.animation_data:
obj.animation_data_clear()
@classmethod
def clear_object_color(cls, obj):
obj.color = (1.0, 1.0, 1.0, 1.0)
@classmethod
def display_object(cls, obj):
if not obj.visible_get():
obj.hide_viewport = False
obj.hide_render = False
@classmethod
def clear_objects_animation(cls, include_blender_objects=True):
for obj in bpy.data.objects:
if not include_blender_objects and not obj.BIMObjectProperties.ifc_definition_id:
continue
cls.clear_object_animation(obj)
if not obj.visible_get():
obj.hide_viewport = False
obj.hide_render = False
cls.clear_object_color(obj)
cls.display_object(obj)
@classmethod
def animate_objects(cls, settings, frames, clear_previous=True, animation_type=""):
@@ -1515,13 +1512,13 @@ class Sequence(blenderbim.core.tool.Sequence):
compare_start = schedule_start
compare_finish = schedule_finish
task_name = task.Name or "Unnamed"
task_name = task_name.replace('\n', "")
task_name = task_name.replace("\n", "")
data = {
"pID": task.id(),
"pName": task_name,
"pCaption": task_name,
"pStart": schedule_start,
"pEnd": schedule_finish ,
"pEnd": schedule_finish,
"pPlanStart": compare_start,
"pPlanEnd": compare_finish,
"pMile": 1 if task.IsMilestone else 0,
@@ -1607,4 +1604,28 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def is_sort_reversed(cls):
return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed
return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed
@classmethod
def get_user_predefined_type(cls):
predefined_type = bpy.context.scene.BIMWorkScheduleProperties.work_schedule_predefined_types
object_type = None
if predefined_type == "USERDEFINED":
object_type = bpy.context.scene.BIMWorkScheduleProperties.object_type
return predefined_type, object_type
@classmethod
def add_animation_camera(cls):
bpy.ops.object.camera_add()
camera = bpy.context.active_object
camera.data.lens = 26
camera.name = "4D Camera"
camera.location = mathutils.Vector((15, 0, 15))
camera.rotation_euler = mathutils.Euler((1.2, 0, 1.5), "XYZ")
for obj in bpy.context.scene.objects:
obj.select_set(False)
for obj in bpy.context.visible_objects:
if not (obj.hide_get() or obj.hide_render) and obj.type != "LIGHT":
obj.select_set(True)
bpy.context.scene.camera = camera
bpy.ops.view3d.camera_to_view_selected()
+39 -1
View File
@@ -21,9 +21,35 @@ import ifcopenshell.util.system
import blenderbim.core.tool
import blenderbim.tool as tool
from blenderbim.bim import import_ifc
import re
from mathutils import Matrix
class System(blenderbim.core.tool.System):
@classmethod
def add_ports(cls, obj, add_start_port=True, add_end_port=True):
def add_port(mep_element, matrix):
port = tool.Ifc.run("system.add_port", element=mep_element)
port.FlowDirection = "NOTDEFINED"
port.PredefinedType = tool.System.get_port_predefined_type(mep_element)
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=matrix, is_si=True)
return port
# make sure obj.dimensions and .matrix_world has valid data
bpy.context.view_layer.update()
# need to make sure .ObjectPlacement is also updated when we're going to add ports
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
mep_element = tool.Ifc.get_entity(obj)
length = obj.dimensions.z
ports = []
if add_start_port:
ports.append(add_port(mep_element, obj.matrix_world @ Matrix()))
if add_end_port:
ports.append(add_port(mep_element, obj.matrix_world @ Matrix.Translation((0, 0, length))))
return ports
@classmethod
def create_empty_at_cursor_with_element_orientation(cls, element):
element_obj = tool.Ifc.get_object(element)
@@ -66,7 +92,19 @@ class System(blenderbim.core.tool.System):
@classmethod
def get_port_relating_element(cls, port):
return port.Nests[0].RelatingObject
if tool.Ifc.get_schema() == "IFC2X3":
element = port.ContainedIn[0].RelatedElement
else:
element = port.Nests[0].RelatingObject
return element
@classmethod
def get_port_predefined_type(cls, mep_element):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(mep_element.is_a())[1:-1]).upper()
if class_name == "CONVEYOR":
return "NOTDEFINED"
return class_name
@classmethod
def import_system_attributes(cls, system):
@@ -3,6 +3,7 @@ Feature: Cost
Scenario: Add cost schedule
Given an empty IFC project
And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN"
When I press "bim.add_cost_schedule"
Then nothing happens
@@ -111,6 +112,53 @@ Scenario: Add cost item
When I press "bim.add_cost_item(cost_item={cost_item})"
Then nothing happens
Scenario: Contract Cost Item
Given an empty IFC project
And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN"
And I press "bim.add_cost_schedule"
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item()"
And I press "bim.add_cost_item(cost_item={cost_item})"
When I press "bim.contract_cost_item(cost_item={cost_item})"
Then nothing happens
Scenario: Contract All Cost Items
Given an empty IFC project
And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN"
And I press "bim.add_cost_schedule"
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item()"
And I press "bim.add_cost_item(cost_item={cost_item})"
When I press "bim.contract_cost_items"
Then nothing happens
Scenario: Expand Cost Item
Given an empty IFC project
And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN"
And I press "bim.add_cost_schedule"
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item()"
And I press "bim.add_cost_item(cost_item={cost_item})"
And I press "bim.contract_cost_item(cost_item={cost_item})"
When I press "bim.expand_cost_item(cost_item={cost_item})"
Then nothing happens
Scenario: Expand All Cost Items
Given an empty IFC project
And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN"
And I press "bim.add_cost_schedule"
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item()"
And I press "bim.add_cost_item(cost_item={cost_item})"
When I press "bim.expand_cost_items"
Then nothing happens
Scenario: Enable editing cost item quantities
Given an empty IFC project
And I press "bim.add_cost_schedule"
@@ -326,7 +326,7 @@ Scenario: See the current frame date as text
And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21"
And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED"
And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w"
And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})"
When I am on frame "1"
Then the object "Timeline" has a body of "2021-01-01"
@@ -357,7 +357,7 @@ Scenario: Animate the construction of a wall
And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21"
And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED"
And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w"
And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})"
When I am on frame "1"
Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "True"
@@ -393,7 +393,7 @@ Scenario: Animate the demolition of a wall
And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21"
And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED"
And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w"
And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})"
When I am on frame "1"
Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "False"
@@ -432,7 +432,7 @@ Scenario: Animate the operation of a wall
And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21"
And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED"
And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w"
And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})"
When I am on frame "1"
Then "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]"
@@ -472,7 +472,7 @@ Scenario: Animate the movement of a wall
And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21"
And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED"
And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w"
And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})"
When I am on frame "1"
Then "scene.objects.get('IfcWall/FromObject').color[:]" is "[1.0, 1.0, 1.0, 1]"
@@ -516,7 +516,7 @@ Scenario: Animate the consumption of a wall
And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21"
And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED"
And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w"
And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})"
When I am on frame "1"
Then "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]"
@@ -532,6 +532,39 @@ Scenario: Animate the consumption of a wall
Then "scene.objects.get('IfcWall/Cube').hide_render" is "True"
Scenario: Clear Previous Animation
Given an empty IFC project
And I press "bim.add_work_schedule"
And the variable "work_schedule" is "IfcStore.get_file().by_type('IfcWorkSchedule')[0].id()"
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION"
And I press "bim.edit_task"
And I press "bim.enable_editing_task_time(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_time_attributes.get('ScheduleStart').string_value" to "2021-01-02"
And I set "scene.BIMWorkScheduleProperties.task_time_attributes.get('ScheduleFinish').string_value" to "2021-01-06"
And I press "bim.edit_task_time"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
And I press "bim.assign_product(task={task})"
And I set "scene.BIMWorkScheduleProperties.visualisation_start" to "01/01/21"
And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21"
And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED"
And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7"
And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w"
And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})"
And I press "bim.clear_previous_animation"
When I am on frame "3"
Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "False"
And "scene.objects.get('IfcWall/Cube').hide_render" is "False"
And "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]"
Scenario: Generate Gantt Chart
Given an empty IFC project
And I press "bim.add_work_schedule"
@@ -808,4 +841,13 @@ Scenario: Duplicate Task and edit sequence Relationship
And I press "bim.assign_successor(task={nested_task_two})"
And I press "bim.duplicate_task(task={task})"
When I press "bim.enable_editing_task_sequence(task={nested_task_one})"
Then nothing happens
Then nothing happens
Scenario: Add Animation Camera
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_animation_camera"
Then "scene.objects.get('4D Camera').name" is "4D Camera"
+12 -38
View File
@@ -58,51 +58,25 @@ class IfcAttributeSetter:
return element
if "." not in key:
return element
if key[0:3] == "Qto":
qto_name, prop = key.split(".", 1)
qto = IfcAttributeSetter.get_element_qto(element, qto_name)
if qto:
IfcAttributeSetter.set_qto_property(qto, prop, value)
return element
pset_name, prop = key.split(".", 1)
pset = IfcAttributeSetter.get_element_pset(element, pset_name)
pset = ifcopenshell.util.element.get_pset(element, pset_name, should_inherit=True)
if pset:
IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value)
return element
pset = ifc_file.by_id(pset["id"])
if pset.is_a("IfcElementQuantity"):
IfcAttributeSetter.set_qto_property(pset, prop, value)
else:
IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value)
return element
@staticmethod
def get_element_qto(element, name):
for relationship in element.IsDefinedBy:
if (
relationship.is_a("IfcRelDefinesByProperties")
and relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity")
and relationship.RelatingPropertyDefinition.Name == name
):
return relationship.RelatingPropertyDefinition
@staticmethod
def set_qto_property(qto, name, value):
for prop in qto.Quantities:
if prop.Name != name:
continue
setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", value)
@staticmethod
def get_element_pset(element, name):
if element.is_a("IfcTypeObject"):
if element.HasPropertySets:
for pset in element.HasPropertySets:
if pset.is_a("IfcPropertySet") and pset.Name == name:
return pset
else:
for relationship in element.IsDefinedBy:
if (
relationship.is_a("IfcRelDefinesByProperties")
and relationship.RelatingPropertyDefinition.is_a("IfcPropertySet")
and relationship.RelatingPropertyDefinition.Name == name
):
return relationship.RelatingPropertyDefinition
try:
setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", float(value))
except:
pass
@staticmethod
def set_pset_property(ifc_file, pset, name, value):
@@ -302,7 +276,7 @@ if __name__ == "__main__":
parser.add_argument("-i", "--ifc", type=str, required=True, help="The IFC file")
parser.add_argument("-s", "--spreadsheet", type=str, default="data.csv", help="The spreadsheet file")
parser.add_argument("-f", "--format", type=str, default="csv", help="The format, chosen from csv, ods, or xlsx")
parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as ".IfcWall"')
parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as "IfcWall"')
parser.add_argument(
"-a",
"--arguments",
@@ -315,7 +289,7 @@ if __name__ == "__main__":
if args.export:
ifc_file = ifcopenshell.open(args.ifc)
results = ifcopenshell.util.selector.Selector.parse(ifc_file, args.query)
results = ifcopenshell.util.selector.filter_elements(ifc_file, args.query)
ifc_csv = IfcCsv()
ifc_csv.export(ifc_file, results, args.arguments or [], output=args.spreadsheet, format=args.format)
elif getattr(args, "import"):
+269
View File
@@ -22,9 +22,278 @@ import ifcopenshell.util.fm
import ifcopenshell.util.selector
import ifcopenshell.util.date
import ifcopenshell.util.schema
import ifcopenshell.util.system
import ifcopenshell.util.placement
import ifcopenshell.util.classification
class Parser2:
def __init__(self, preset="BASIC"):
self.file = None
self.categories = {}
self.get_category_elements = {}
self.get_element_data = {}
self.get_custom_element_data = {}
self.duplicate_keys = []
if preset == "BASIC":
self.get_category_elements = {
"actors": get_actors,
"facilities": get_facilities,
"storeys": get_storeys,
"spaces": get_spaces,
"zones": get_zones,
"types": get_types,
"elements": get_elements,
"systems": get_systems,
}
self.get_element_data = {
"actors": get_actor_data,
"facilities": get_facility_data,
"storeys": get_storey_data,
"spaces": get_space_data,
"zones": get_zone_data,
"types": get_type_data,
"elements": get_element_data,
"systems": get_system_data,
}
def parse(self, ifc_file):
for category_name, get_category_elements in self.get_category_elements.items():
self.categories.setdefault(category_name, {})
for element in get_category_elements(ifc_file):
data = self.get_element_data[category_name](ifc_file, element) or {}
custom_data = (
self.get_custom_element_data.get(category_name, lambda x, y: None)(ifc_file, element) or {}
)
data.update(custom_data)
if data:
if data["key"] in self.categories[category_name]:
self.duplicate_keys.append((self.categories[category_name][data["key"]], data))
self.categories[category_name][data["key"]] = data
def get_actors(ifc_file):
return ifc_file.by_type("IfcActor")
def get_facilities(ifc_file):
return ifc_file.by_type("IfcBuilding")
def get_storeys(ifc_file):
return ifc_file.by_type("IfcBuildingStorey")
def get_spaces(ifc_file):
return ifc_file.by_type("IfcSpace")
def get_zones(ifc_file):
zones = []
for zone in ifc_file.by_type("IfcZone"):
for rel in zone.IsGroupedBy:
zones.extend([(zone, space) for space in rel.RelatedObjects])
return zones
def get_types(ifc_file):
return ifcopenshell.util.fm.get_fmhem_types(ifc_file)
def get_elements(ifc_file):
elements = set()
for element_type in ifcopenshell.util.fm.get_fmhem_types(ifc_file):
elements.update(ifcopenshell.util.element.get_types(element_type))
return elements
def get_systems(ifc_file):
return ifc_file.by_type("IfcSystem")
def get_actor_data(ifc_file, element):
return {
"key": element.TheActor.Name,
"Name": element.TheActor.Name,
"Category": get_classification(element),
"Email": get_actor_address(element, "ElectronicMailAddresses"),
"Phone": get_actor_address(element, "TelephoneNumbers"),
"CompanyURL": get_actor_address(element, "WWWHomePageURL"),
"Department": get_actor_address(element, "InternalLocation"),
"Address1": get_actor_address(element, "AddressLines"),
"Address2": get_actor_address(element, "Town"),
"StateRegion": get_actor_address(element, "Region"),
"PostalCode": get_actor_address(element, "PostalCode"),
"Country": get_actor_address(element, "Country"),
}
def get_facility_data(ifc_file, element):
return {
"key": element.Name,
"Name": element.Name,
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(ifc_file.by_type("IfcProject")[0]),
"Category": get_classification(element),
"ProjectName": ifc_file.by_type("IfcProject")[0].Name,
"SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None),
"LinearUnits": "millimeters",
"AreaUnits": "square meters",
"AreaMeasurement": "BIM Software",
"Phase": ifc_file.by_type("IfcProject")[0].Phase,
"ModelSoftware": get_owner_application(element),
"ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId,
"ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None),
"ModelBuildingID": element.GlobalId,
}
def get_storey_data(ifc_file, element):
return {
"key": element.Name,
"Name": element.Name,
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"Category": "Level",
"ModelSoftware": get_owner_application(element),
"ModelObject": element.is_a(),
"ModelID": element.GlobalId,
"Elevation": ifcopenshell.util.placement.get_storey_elevation(element),
}
def get_space_data(ifc_file, element):
psets = ifcopenshell.util.element.get_psets(element)
return {
"key": element.Name,
"Name": element.Name,
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"Category": get_classification(element),
"LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None),
"Description": element.LongName,
"ModelSoftware": get_owner_application(element),
"ModelID": element.GlobalId,
"AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2),
"AreaNet": get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2),
}
def get_zone_data(ifc_file, element):
zone, space = element
return {
"key": (element.Name or "Unnamed") + (space.Name or "Unnamed"),
"Name": zone.Name,
"AuthorOrganizationName": get_owner_name(zone),
"AuthorDate": get_owner_creation_date(zone),
"SpaceName": space.Name,
"ModelSoftware": get_owner_application(zone),
"ModelID": zone.GlobalId,
}
def get_type_data(ifc_file, element):
return {
"key": element.Name,
"Name": element.Name,
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"Category": get_classification(element),
"Description": element.Description,
"ModelSoftware": get_owner_application(element),
"ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
"ModelTag": element.Tag,
"ModelID": element.GlobalId,
}
def get_element_data(ifc_file, element):
space = ifcopenshell.util.element.get_container(element)
space_name = space.Name if space.is_a("IfcSpace") else None
systems = ifcopenshell.util.system.get_element_systems(element)
system = systems[0].Name if systems else None
return {
"key": element.Name,
"Name": element.Name,
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"TypeName": ifcopenshell.util.element.get_type(element).Name,
"SpaceName": space_name,
"SystemName": system,
"ModelSoftware": get_owner_application(element),
"ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
"ModelID": element.GlobalId,
}
def get_system_data(ifc_file, element):
return {
"key": element.Name,
"Name": element.Name,
"Description": element.Description,
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"Category": get_classification(element),
"ModelSoftware": get_owner_application(element),
"ModelID": element.GlobalId,
}
def get_owner_name(element):
if not getattr(element, "OwnerHistory", None):
return
return element.OwnerHistory.OwningUser.TheOrganization.Name
def get_owner_creation_date(element):
if not getattr(element, "OwnerHistory", None):
return
return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat()
def get_owner_application(element):
if not getattr(element, "OwnerHistory", None):
return
return element.OwnerHistory.OwningApplication.ApplicationFullName
def get_facility_parent(element, ifc_class):
parent = ifcopenshell.util.element.get_aggregate(element)
while parent:
if parent.is_a(ifc_class):
return parent
if parent.is_a("IfcProject"):
return
parent = ifcopenshell.util.element.get_aggregate(parent)
def get_classification(element):
references = list(ifcopenshell.util.classification.get_references(element))
if references:
if hasattr(references[0], "Identification"):
return "{}:{}".format(references[0].Identification, references[0].Name)
return "{}:{}".format(references[0].ItemReference, references[0].Name)
def get_actor_address(element, name):
for address in element.TheActor.Addresses or []:
if hasattr(address, name) and getattr(address, name, None):
result = getattr(address, name)
if isinstance(result, tuple):
return result[0]
return result
def get_property(psets, pset_name, prop_name, decimals=None):
if pset_name in psets:
result = psets[pset_name].get(prop_name, None)
if decimals is None or result is None:
return result
return round(result, decimals)
class Parser:
def __init__(self, logger):
self.logger = logger
@@ -428,6 +428,8 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
for (int i = 1; i <= n; ++i) {
gp_XYZ p = tessellater.Value(i).XYZ();
auto p_local = p;
trsf.Transforms(p);
int current = addVertex(iit->ItemId(), surface_style_id, p);
@@ -455,11 +457,10 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
}
d3 = d1.XYZ() + d2.XYZ();
d4 = d1.XYZ() - d2.XYZ();
p2 = p - d3.XYZ() / 10.;
p3 = p - d4.XYZ() / 10.;
p2 = p_local - d3.XYZ() / 10.;
p3 = p_local - d4.XYZ() / 10.;
trsf.Transforms(p2);
trsf.Transforms(p3);
trsf.Transforms(p);
int left = addVertex(iit->ItemId(), surface_style_id, p2);
int right = addVertex(iit->ItemId(), surface_style_id, p3);
@@ -1,16 +1,19 @@
Installation
============
There are different methods of installation, depending on your situation.
There are different methods of installation, depending on your situation. If
you aren't sure which to choose, if you're a programmer, go for the **Pre-built
packages**. If you aren't a programmer, go for the **BlenderBIM Add-on**.
1. **Pre-built packages** is recommended for users wanting to use the latest IfcOpenShell builds.
2. **PyPI** is recommended for developers using Pip.
3. **Conda** is recommended for developers using Anaconda.
4. **Docker** is recommended for developers using Docker.
5. **AWS Lambda** is recommended for developers using AWS Lambda functions.
6. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface.
7. **From source with precompiled binaries** is recommended for developers actively working with the Python code.
8. **Compiling from source** is recommended for developers actively working with the C++ core.
6. **Google Colab** is recommended for developers using Google Colab.
7. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface.
8. **From source with precompiled binaries** is recommended for developers actively working with the Python code.
9. **Compiling from source** is recommended for developers actively working with the C++ core.
Pre-built packages
------------------
@@ -182,6 +185,18 @@ Gateways, etc.
the AWS documentation. Some tools that could be useful are AWS
CloudFormaton, AWS CDK, pulumi or terraform.
Google Colab
------------
The Google Colab environment is based on the distribution from PyPI, but lets
you run it in an online notebook without any local setup required. This is
great for educators and those wanting to try it out without control on their
local system.
`Click here
<https://colab.research.google.com/drive/1S9uZQvqXRpF1z6JTiKk79M1Ln63rHHIZ?usp=sharing>`__
to launch a simple notebook.
Using the BlenderBIM Add-on
---------------------------
@@ -77,28 +77,41 @@ class Usecase:
cost_item=item, products=[slab], prop_name="NetVolume")
"""
self.file = file
self.settings = {"cost_item": cost_item, "products": products or [], "prop_name": prop_name}
self.settings = {
"cost_item": cost_item,
"products": products or [],
"prop_name": prop_name,
}
def execute(self):
if self.settings["prop_name"]:
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for product in self.settings["products"]:
ifcopenshell.api.run(
"control.assign_control",
self.file,
related_object=product,
relating_control=self.settings["cost_item"],
self.assign_cost_control(
related_object=product, cost_item=self.settings["cost_item"]
)
if self.settings["prop_name"]:
if (
self.settings["cost_item"].CostQuantities
and self.settings["cost_item"].CostQuantities[0].Name.lower()
!= self.settings["prop_name"].lower()
) or not product.is_a("IfcObject"):
continue
self.add_quantity_from_related_object(product)
if self.settings["prop_name"]:
self.settings["cost_item"].CostQuantities = list(self.quantities)
else:
self.update_cost_item_count()
def assign_cost_control(self, related_object, cost_item):
return ifcopenshell.api.run(
"control.assign_control",
self.file,
related_object=related_object,
relating_control=cost_item,
)
def add_quantity_from_related_object(self, element):
if not element.is_a("IfcObject"):
return
for relationship in element.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
self.add_quantity_from_qto(relationship.RelatingPropertyDefinition)
@@ -107,14 +120,17 @@ class Usecase:
if not qto.is_a("IfcElementQuantity"):
return
for prop in qto.Quantities:
if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == self.settings["prop_name"].lower():
if (
prop.is_a("IfcPhysicalSimpleQuantity")
and prop.Name.lower() == self.settings["prop_name"].lower()
):
self.quantities.add(prop)
def update_cost_item_count(self):
# This is a bold assumption
# https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564
if not self.settings["cost_item"].CostQuantities:
return ifcopenshell.api.run(
ifcopenshell.api.run(
"cost.add_cost_item_quantity",
self.file,
cost_item=self.settings["cost_item"],
@@ -98,4 +98,7 @@ class Usecase:
count = 0
for rel in self.settings["cost_item"].Controls:
count += len(rel.RelatedObjects)
quantity[3] = count
if count:
quantity[3] = count
else:
self.file.remove(quantity)
@@ -46,10 +46,17 @@ class Usecase:
def execute(self):
for reference in self.settings["information"].HasDocumentReferences or []:
ifcopenshell.api.run("document.remove_reference", self.file, reference=reference)
for rel in self.settings["information"].IsPointer or []:
for information in rel.RelatedDocuments:
ifcopenshell.api.run("document.remove_information", self.file, information=information)
self.file.remove(rel)
# remove IfcDocumentInformationRelationship so it won't become invalid
for rel in self.settings["information"].IsPointedTo or []:
if rel.RelatedDocuments == (self.settings["information"],):
self.file.remove(rel)
for rel in self.settings["information"].DocumentInfoForObjects or []:
self.file.remove(rel)
self.file.remove(self.settings["information"])
@@ -63,7 +63,11 @@ class Usecase:
def apply_clippings(self, first_operand):
while self.settings["clippings"]:
clipping = self.settings["clippings"].pop()
if clipping["operand_type"] == "IfcHalfSpaceSolid":
if isinstance(clipping, ifcopenshell.entity_instance):
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
elif clipping["operand_type"] == "IfcHalfSpaceSolid":
matrix = clipping["matrix"]
second_operand = self.file.createIfcHalfSpaceSolid(
self.file.createIfcPlane(
@@ -81,7 +85,7 @@ class Usecase:
),
False,
)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
return first_operand
def convert_si_to_unit(self, co):
@@ -95,7 +95,11 @@ class Usecase:
def apply_clippings(self, first_operand):
while self.settings["clippings"]:
clipping = self.settings["clippings"].pop()
if clipping["operand_type"] == "IfcHalfSpaceSolid":
if isinstance(clipping, ifcopenshell.entity_instance):
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
elif clipping["operand_type"] == "IfcHalfSpaceSolid":
matrix = clipping["matrix"]
second_operand = self.file.createIfcHalfSpaceSolid(
self.file.createIfcPlane(
@@ -113,7 +117,7 @@ class Usecase:
),
False,
)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
return first_operand
def convert_si_to_unit(self, co):
@@ -22,7 +22,7 @@ import ifcopenshell.util.unit
class Usecase:
def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None):
def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None, is_si=True):
self.file = file
self.settings = {
"element": element,
@@ -32,15 +32,25 @@ class Usecase:
"elevation": elevation,
"height": height,
"thickness": thickness,
"is_si": is_si
}
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["p1"] = np.array(self.settings["p1"])
self.settings["p2"] = np.array(self.settings["p2"])
self.settings["p1"] = np.array(self.settings["p1"]).astype(float)
self.settings["p2"] = np.array(self.settings["p2"]).astype(float)
length = float(np.linalg.norm(self.settings["p2"] - self.settings["p1"]))
if not self.settings["is_si"]:
length=self.convert_unit_to_si(length)
self.settings["height"]=self.convert_unit_to_si(self.settings["height"])
self.settings["thickness"]=self.convert_unit_to_si(self.settings["thickness"])
self.settings["p1"][0] = self.convert_unit_to_si(self.settings["p1"][0])
self.settings["p1"][1] = self.convert_unit_to_si(self.settings["p1"][1])
self.settings["elevation"] = self.convert_unit_to_si(self.settings["elevation"])
representation = ifcopenshell.api.run(
"geometry.add_wall_representation",
self.file,
@@ -55,7 +65,7 @@ class Usecase:
[
[v[0], -v[1], 0, self.settings["p1"][0]],
[v[1], v[0], 0, self.settings["p1"][1]],
[0, 0, 1, self.convert_si_to_unit(self.settings["elevation"])],
[0, 0, 1, self.settings["elevation"]],
[0, 0, 0, 1],
]
)
@@ -64,7 +74,5 @@ class Usecase:
)
return representation
def convert_si_to_unit(self, co):
if isinstance(co, (tuple, list)):
return [self.convert_si_to_unit(o) for o in co]
return co / self.settings["unit_scale"]
def convert_unit_to_si(self, co):
return co * self.settings["unit_scale"]
@@ -69,7 +69,7 @@ class Usecase:
outer_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in outer_points])
inner_curves = []
for inner_point in inner_points:
inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point])
inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point]))
else:
outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points))
inner_curves = []
@@ -172,6 +172,7 @@ class Usecase:
should_run_listeners=False,
related_object=element,
relating_type=new_type,
should_map_representations=False,
)
ifcopenshell.api.owner.settings.restore()
@@ -65,13 +65,29 @@ class Usecase:
representations = self.settings["product"].Representation.Representations or []
else:
representations = []
# remove object placements
object_placement = self.settings["product"].ObjectPlacement
if object_placement and self.file.get_total_inverses(object_placement) == 1:
self.settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work
ifcopenshell.util.element.remove_deep2(self.file, object_placement)
if object_placement:
if self.file.get_total_inverses(object_placement) == 1:
self.settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work
ifcopenshell.util.element.remove_deep2(self.file, object_placement)
elif self.settings["product"].is_a("IfcTypeProduct"):
representations = [rm.MappedRepresentation for rm in self.settings["product"].RepresentationMaps or []]
# remove psets
psets = self.settings["product"].HasPropertySets or []
for pset in psets:
if self.file.get_total_inverses(pset) != 1:
continue
ifcopenshell.api.run(
"pset.remove_pset",
self.file,
product=self.settings["product"],
pset=pset,
)
for representation in representations:
ifcopenshell.api.run(
"geometry.unassign_representation",
@@ -27,6 +27,7 @@ class Usecase:
file,
name="Unnamed",
predefined_type="NOTDEFINED",
object_type=None,
start_time=None,
work_plan=None,
):
@@ -77,6 +78,7 @@ class Usecase:
self.settings = {
"name": name,
"predefined_type": predefined_type,
"object_type": object_type,
"start_time": start_time or datetime.now(),
"work_plan": work_plan,
}
@@ -98,7 +100,8 @@ class Usecase:
work_schedule.StartTime = ifcopenshell.util.date.datetime2ifc(
self.settings["start_time"], "IfcDateTime"
)
if self.settings["object_type"]:
work_schedule.ObjectType = self.settings["object_type"]
if self.settings["work_plan"]:
ifcopenshell.api.run(
"aggregate.assign_object",
@@ -22,7 +22,7 @@ import ifcopenshell.util.element
class Usecase:
def __init__(self, file, related_object=None, relating_type=None):
def __init__(self, file, related_object=None, relating_type=None, should_map_representations=True):
"""Assigns a type to an occurrence of an object
IFC supports the concept of occurrences and types. An occurrence is an
@@ -87,6 +87,11 @@ class Usecase:
:type related_object: ifcopenshell.entity_instance.entity_instance
:param relating_type: The IfcElementType type.
:type relating_type: ifcopenshell.entity_instance.entity_instance
:param should_map_representations: If a type has a representation map,
IFC requires all occurrences to map those representations. Some IFC
vendors might disobey this, or you might want to handle it
yourself. In this scenario, you may set this to False.
:type should_map_representations: bool
:return: The IfcRelDefinesByType relationship
:rtype: ifcopenshell.entity_instance.entity_instance
@@ -164,6 +169,7 @@ class Usecase:
self.settings = {
"related_object": related_object,
"relating_type": relating_type,
"should_map_representations": should_map_representations,
}
def execute(self):
@@ -207,14 +213,15 @@ class Usecase:
}
)
if getattr(self.settings["relating_type"], "RepresentationMaps", None):
ifcopenshell.api.run(
"type.map_type_representations",
self.file,
related_object=self.settings["related_object"],
relating_type=self.settings["relating_type"],
)
self.map_material_usages()
if self.settings["should_map_representations"]:
if getattr(self.settings["relating_type"], "RepresentationMaps", None):
ifcopenshell.api.run(
"type.map_type_representations",
self.file,
related_object=self.settings["related_object"],
relating_type=self.settings["relating_type"],
)
self.map_material_usages()
return types
def map_material_usages(self):
@@ -60,6 +60,7 @@ class draw_settings:
merge_cells: bool = False
include_projection: bool = True
prefilter: bool = True
include_curves: bool = False
def main(settings, files, iterators=None, merge_projection=True, progress_function=DO_NOTHING):
@@ -68,6 +69,7 @@ def main(settings, files, iterators=None, merge_projection=True, progress_functi
# this is required for serialization
APPLY_DEFAULT_MATERIALS=True,
DISABLE_TRIANGULATION=True,
INCLUDE_CURVES=settings.include_curves,
# when not doing booleans, proper solids from shells isn't a requirement
SEW_SHELLS=settings.subtract_before_hlr,
)
@@ -339,6 +339,8 @@ def get_material(element, should_skip_usage=False, should_inherit=True):
The material may be a single material, material set (layered, profiled, or
constituent), or a material set usage.
:param element: The element to get the material of.
:type element: ifcopenshell.entity_instance.entity_instance
:param should_skip_usage: If set to True, if the material is a material set
usage, the material set itself will be returned. Useful if you don't
care about occurrence usage parameters. If False, the usage will be
@@ -378,6 +380,8 @@ def get_materials(element, should_inherit=True):
If the element has a material set, the individual materials of that set are
returned as a list.
:param element: The element to get the materials of.
:type element: ifcopenshell.entity_instance.entity_instance
:param should_inherit: If True, any inherited materials from associated
types will be considered.
:return: The associated materials of the element.
@@ -403,6 +407,51 @@ def get_materials(element, should_inherit=True):
return [c.Material for c in material.MaterialConstituents]
def get_styles(element):
"""Retrieves the styles used in an element's representation.
Styles may be retreived from the material or the body representation.
:param element: The element to get the styles of.
:type element: ifcopenshell.entity_instance.entity_instance
:return: A list of surface styles
:rtype: list[ifcopenshell.entity_instance.entity_instance]
Example:
.. code:: python
wall = file.by_type("IfcWall")[0]
styles = ifcopenshell.util.element.get_styles(wall)
"""
styles = []
materials = ifcopenshell.util.element.get_materials(element)
for material in materials:
for material_definition_representation in material.HasRepresentation or []:
for representation in material_definition_representation.Representations:
for item in representation.Items:
styles.extend([s for s in item.Styles if s.is_a("IfcSurfaceStyle")])
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not body:
return styles
for representation in [body]:
queue = list(representation.Items)
while queue:
item = queue.pop()
if item.is_a("IfcMappedItem"):
queue.extend(item.MappingSource.MappedRepresentation.Items)
if item.is_a("IfcBooleanResult"):
queue.append(item.FirstOperand)
queue.append(item.SecondOperand)
if item.StyledByItem:
styles.extend([s for s in item.StyledByItem[0].Styles if s.is_a("IfcSurfaceStyle")])
return styles
def get_elements_by_material(ifc_file, material):
"""Retrieves the elements related to a material.
@@ -294,7 +294,9 @@ class FacetTransformer(lark.Transformer):
return False
def compare(self, element_value, comparison, value):
if isinstance(value, str):
if isinstance(element_value, (list, tuple)):
return any(self.compare(ev, comparison, value) for ev in element_value)
elif isinstance(value, str):
if isinstance(element_value, int):
value = int(value)
elif isinstance(element_value, float):
@@ -520,6 +522,10 @@ class Selector:
value = ifcopenshell.util.element.get_type(value)
elif key in ("material", "mat"):
value = ifcopenshell.util.element.get_material(value, should_skip_usage=True)
elif key in ("materials", "mats"):
value = ifcopenshell.util.element.get_materials(value)
elif key == "styles":
value = ifcopenshell.util.element.get_styles(value)
elif key in ("item", "i"):
if value.is_a("IfcMaterialLayerSet"):
value = value.MaterialLayers
@@ -19,7 +19,7 @@
import collections
import ifcopenshell
import ifcopenshell.api
from math import cos, sin, pi
from math import cos, sin, pi, tan, radians
from mathutils import Vector, Matrix
from itertools import chain
@@ -539,7 +539,7 @@ class ShapeBuilder:
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation"
)
if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"):
if not profile_or_curve.is_a("IfcProfileDef"):
profile_or_curve = self.profile(profile_or_curve)
if position_y_axis:
@@ -579,6 +579,8 @@ class ShapeBuilder:
representation_type = "AdvancedSweptSolid"
elif "IfcExtrudedAreaSolid" in item_types:
representation_type = "SweptSolid"
elif items[0].is_a("IfcTessellatedItem"):
representation_type = "Tessellation"
elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
representation_type = "Curve3D"
else:
@@ -746,8 +748,10 @@ class ShapeBuilder:
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return (points, segments, ifc_curve)
def create_z_profile_lips_curve(self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius):
def create_z_profile_lips_curve(
self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius
):
x1 = FirstFlangeWidth
x2 = SecondFlangeWidth
y = Depth / 2
@@ -770,20 +774,21 @@ class ShapeBuilder:
(-x1+t, -y+t),
(-t/2, -y+t)
)
# fmt: on
# option for no additional thickness in outer radius:
# points, segments, ifc_curve = create_curve_from_coords(
# coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=r, closed=True, ifc_file=ifc_file
# )
points, segments, ifc_curve = self.get_simple_2dcurve_data(coords,
points, segments, ifc_curve = self.get_simple_2dcurve_data(
coords,
fillets = (0, 1, 4, 5, 6, 7, 10, 11),
fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r),
closed=True, create_ifc_curve=True)
# fmt: on
return ifc_curve
def create_transition_arc_ifc(self, width, height, create_ifc_curve=False):
# create an arc in the rectangle with specified width and height
# if it's not possible to make a complete arc
@@ -814,4 +819,114 @@ class ShapeBuilder:
points, segments, transition_arc = self.get_simple_2dcurve_data(
curve_coords, fillets, fillet_radius, closed=False, create_ifc_curve=create_ifc_curve
)
return points, segments, transition_arc
return points, segments, transition_arc
def polygonal_face_set(self, points, faces):
"""
> `points` - list of points
> `faces` - list of faces consisted of point indices (points indices starting from 0)
< IfcPolygonalFaceSet
"""
ifc_points = self.file.createIfcCartesianPointList3D(points)
ifc_faces = []
for face in faces:
face = [i + 1 for i in face]
ifc_faces.append(self.file.createIfcIndexedPolygonalFace(face))
face_set = self.file.createIfcPolygonalFaceSet(Coordinates=ifc_points, Faces=ifc_faces)
return face_set
def mep_transition_shape(self, start_segment, end_segment, start_length, end_length, angle=30.0):
"""
returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data
"""
# good default values from angle = 30/60 deg
# 30 degree angle will result in 75 degrees on the transition (= 90 - α/2) - https://i.imgur.com/tcoYDWu.png
# TODO: get rid of reliance on profiles
def get_profile(element):
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1:
return material.MaterialProfiles[0].Profile
start_profile = get_profile(start_segment)
end_profile = get_profile(end_segment)
# TODO: support more profiles
if not start_profile.is_a("IfcRectangleProfileDef") or not end_profile.is_a("IfcRectangleProfileDef"):
# Non rectangular profiles are not yet supported
return None, None
start_half_dim = V(start_profile.XDim / 2, start_profile.YDim / 2, start_length)
end_half_dim = V(end_profile.XDim / 2, end_profile.YDim / 2, end_length)
transition_items = []
end_extrusion_offset = V(0, 0, start_length)
def get_transition_legth(start_half_dim, end_half_dim, angle):
diff = start_half_dim.xy - end_half_dim.xy
diff = Vector([abs(i) for i in diff])
c = diff.x * tan(radians(90 - angle / 2))
a = diff.y
b = (c**2 - a**2) ** 0.5
return b
transition_length = get_transition_legth(start_half_dim, end_half_dim, angle)
faces = []
if transition_length != 0:
end_extrusion_offset.z += transition_length
faces += [(3, 4, 7, 0), (11, 8, 15, 12), (3, 11, 12, 4), (7, 15, 8, 0)]
# NOTE: clockwise order for correct face orientation
faces += [
# start extrusion
(0, 1, 2, 3),
(8, 11, 10, 9),
(0, 8, 9, 1),
(1, 9, 10, 2),
(2, 10, 11, 3),
# end extrusion
(4, 5, 6, 7),
(12, 15, 14, 13),
(4, 12, 13, 5),
(5, 13, 14, 6),
(6, 14, 15, 7),
]
points = [
start_half_dim * V(-1, -1, 1),
start_half_dim * V(-1, -1, 0),
start_half_dim * V(1, -1, 0),
start_half_dim * V(1, -1, 1),
end_half_dim * V(1, -1, 0) + end_extrusion_offset,
end_half_dim * V(1, -1, 1) + end_extrusion_offset,
end_half_dim * V(-1, -1, 1) + end_extrusion_offset,
end_half_dim * V(-1, -1, 0) + end_extrusion_offset,
start_half_dim * V(-1, 1, 1),
start_half_dim * V(-1, 1, 0),
start_half_dim * V(1, 1, 0),
start_half_dim * V(1, 1, 1),
end_half_dim * V(1, 1, 0) + end_extrusion_offset,
end_half_dim * V(1, 1, 1) + end_extrusion_offset,
end_half_dim * V(-1, 1, 1) + end_extrusion_offset,
end_half_dim * V(-1, 1, 0) + end_extrusion_offset,
]
face_set = self.polygonal_face_set(points, faces)
transition_items.append(face_set)
body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW")
representation = self.get_representation(body, transition_items, "Tesselation")
transition_data = {
"start_length": start_length,
"end_length": end_length,
"angle": angle,
"transition_length": transition_length,
"full_transition_length": start_length + transition_length + end_length,
}
return representation, transition_data
@@ -37,6 +37,19 @@ class TestRemoveInformation(test.bootstrap.IFC4):
assert len(self.file.by_type("IfcDocumentReference")) == 0
assert len(self.file.by_type("IfcRelAssociatesDocument")) == 0
# test removing relationship to another information if it was the only relating element
information = ifcopenshell.api.run("document.add_information", self.file, parent=None)
information1 = ifcopenshell.api.run("document.add_information", self.file, parent=information)
information2 = ifcopenshell.api.run("document.add_information", self.file, parent=information)
ifcopenshell.api.run("document.remove_information", self.file, information=information1)
assert len(self.file.by_type("IfcDocumentInformation")) == 2
assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 1
ifcopenshell.api.run("document.remove_information", self.file, information=information2)
assert len(self.file.by_type("IfcDocumentInformation")) == 1
assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 0
def test_removing_all_subdocuments_and_their_references_too(self):
project = self.file.createIfcProject()
information = ifcopenshell.api.run("document.add_information", self.file, parent=None)
@@ -54,6 +54,24 @@ class TestRemoveProduct(test.bootstrap.IFC4):
ifcopenshell.api.run("root.remove_product", self.file, product=element1)
assert len(self.file.by_type("IfcObjectPlacement")) == 0
def test_removing_element_type_psets(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType")
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"})
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType")
element2.HasPropertySets = (pset,)
# make sure it won't remove the pset if it's connected elsewhere
ifcopenshell.api.run("root.remove_product", self.file, product=element2)
assert len(self.file.by_type("IfcPropertySet")) == 1
assert len(self.file.by_type("IfcPropertySingleValue")) == 1
# if it's the product is the only inverse for pset, it should remove the pset
ifcopenshell.api.run("root.remove_product", self.file, product=element)
assert len(self.file.by_type("IfcPropertySet")) == 0
assert len(self.file.by_type("IfcPropertySingleValue")) == 0
def test_removing_all_representations_of_an_element(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
ifcopenshell.api.run("unit.assign_unit", self.file)
@@ -386,6 +386,53 @@ class TestGetMaterial(test.bootstrap.IFC4):
assert subject.get_material(element, should_inherit=False) is None
class TestGetMaterials(test.bootstrap.IFC4):
def test_getting_the_materials_of_a_product(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
material = ifcopenshell.api.run("material.add_material", self.file)
ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material)
assert subject.get_materials(element) == [material]
class TestGetStyles(test.bootstrap.IFC4):
def test_getting_the_styles_of_a_product(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
assert subject.get_styles(element) == []
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
body = ifcopenshell.api.run("context.add_context", self.file,
context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model)
material = ifcopenshell.api.run("material.add_material", self.file)
ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material)
style = ifcopenshell.api.run("style.add_style", self.file)
ifcopenshell.api.run("style.add_surface_style", self.file,
style=style, ifc_class="IfcSurfaceStyleShading", attributes={
"SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 },
"Transparency": 0., # 0 is opaque, 1 is transparent
})
ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=body)
assert subject.get_styles(element) == [style]
style2 = ifcopenshell.api.run("style.add_style", self.file)
ifcopenshell.api.run("style.add_surface_style", self.file,
style=style2, ifc_class="IfcSurfaceStyleShading", attributes={
"SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 },
"Transparency": 0., # 0 is opaque, 1 is transparent
})
representation = ifcopenshell.api.run("geometry.add_wall_representation", self.file,
context=body, length=5, height=3, thickness=0.118)
ifcopenshell.api.run("geometry.assign_representation", self.file, product=element, representation=representation)
ifcopenshell.api.run("style.assign_representation_styles", self.file, shape_representation=representation, styles=[style2])
assert subject.get_styles(element) == [style, style2]
class TestGetElementsByMaterial(test.bootstrap.IFC4):
def test_getting_elements_of_a_material(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
@@ -137,6 +137,9 @@ class TestFilterElements(test.bootstrap.IFC4):
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Baz": 123})
assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz=123") == {element}
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Bay": 123.3})
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["New"]})
assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element}
def test_selecting_by_classification(self):
project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
+15 -1
View File
@@ -21,6 +21,8 @@ import sys
import math
import logging
import datetime
import ifcopenshell
import ifcopenshell.util.element
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -163,6 +165,7 @@ class Json(Reporter):
return self.results
def report_specification(self, specification):
applicability = [a.to_string("applicability") for a in specification.applicability]
requirements = []
for requirement in specification.requirements:
requirements.append(
@@ -182,12 +185,23 @@ class Json(Reporter):
"total": total,
"percentage": percentage,
"required": specification.minOccurs != 0,
"applicability": applicability,
"requirements": requirements,
}
def report_failed_entities(self, requirement):
return [
{"reason": requirement.failed_reasons[i], "element": str(e)}
{
"reason": requirement.failed_reasons[i],
"element": str(e),
"class": e.is_a(),
"predefined_type": ifcopenshell.util.element.get_predefined_type(e),
"name": getattr(e, "Name", None),
"description": getattr(e, "Description", None),
"id": e.id(),
"global_id": getattr(e, "GlobalId", None),
"tag": getattr(e, "Tag", None),
}
for i, e in enumerate(requirement.failed_entities)
]
+23 -2
View File
@@ -761,7 +761,14 @@ void SvgSerializer::write(const geometry_data& data) {
// SVG has a coordinate system with the origin in the *upper*-left corner
// therefore we mirror the shape along the XZ-plane.
gp_Trsf trsf_mirror;
trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY()));
if (!mirror_y_) {
trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY()));
}
if (mirror_x_) {
gp_Trsf mirror_x;
mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX()));
trsf_mirror.PreMultiply(mirror_x);
}
BRepBuilderAPI_Transform make_transform_mirror(compound_unmirrored, trsf_mirror, true);
make_transform_mirror.Build();
// (When determinant < 0, copy is implied and the input is not mutated.)
@@ -1667,6 +1674,13 @@ std::array<std::array<double, 3>, 3> SvgSerializer::resize() {
cy = ymin * sc;
}
if (mirror_y_) {
cy = - size_->second - cy;
}
if (mirror_x_) {
cx = - size_->first - cx;
}
m = {{ {{sc,0,-cx}},{{0,sc,-cy}},{{0,0,1}} }};
float_item_list::const_iterator it;
@@ -1704,7 +1718,14 @@ void SvgSerializer::draw_hlr(const gp_Pln& pln, const drawing_key& drawing_name)
TopoDS_Shape hlr_compound;
if (drawing_name.first == nullptr) {
gp_Trsf trsf_mirror;
trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY()));
if (!mirror_y_) {
trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY()));
}
if (mirror_x_) {
gp_Trsf mirror_x;
mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX()));
trsf_mirror.PreMultiply(mirror_x);
}
BRepBuilderAPI_Transform make_transform_mirror(hlr_compound_unmirrored, trsf_mirror, true);
make_transform_mirror.Build();
hlr_compound = make_transform_mirror.Shape();
+28 -3
View File
@@ -309,11 +309,16 @@ namespace {
}
} else {
gp_Pnt2d tmp;
for (int i = 0; i < 4; ++i) {
// 0,1,2,3 -> interp over bounding box edges (i%4, (i+1)%4)
// 4,5 -> interp over bounding box diagonals (i%4, (i+2)%4)
// @todo use boolean_utils.h points_on_planar_face_generator?
// ... or skip faces with inner bounds all together ?
// ... ?
for (int i = 0; i < 6; ++i) {
// @todo proper edge intersection
for (int j = 0; j < 16; ++j) {
const gp_Pnt2d& a = *loop[i];
const gp_Pnt2d& b = *loop[(i + 1) % 4];
const gp_Pnt2d& a = *loop[i % 4];
const gp_Pnt2d& b = *loop[(i + (i >= 4 ? 2 : 1)) % 4];
interp(a, b, j / 16.0, tmp);
if (fclass->Perform(tmp) == TopAbs_OUT) {
return false;
@@ -516,6 +521,8 @@ protected:
bool emit_building_storeys_;
bool no_css_;
bool unify_inputs_;
bool mirror_y_;
bool mirror_x_;
int profile_threshold_;
@@ -567,6 +574,8 @@ public:
, polygonal_(false)
, emit_building_storeys_(true)
, no_css_(false)
, mirror_y_(false)
, mirror_x_(false)
, unify_inputs_(false)
, profile_threshold_(-1)
, file(0)
@@ -708,6 +717,22 @@ public:
return profile_threshold_;
}
void setMirrorY(bool b) {
mirror_y_ = b;
}
bool getMirrorY() const {
return mirror_y_;
}
void setMirrorX(bool b) {
mirror_x_ = b;
}
bool getMirrorX() const {
return mirror_x_;
}
protected:
std::string writeMetadata(const drawing_meta& m);
};