mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0
This commit is contained in:
@@ -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%" ^
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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"):
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user