diff --git a/.github/workflows/ci-bcf-pypi.yml b/.github/workflows/ci-bcf-pypi.yml new file mode 100644 index 0000000000..ae0a3a8900 --- /dev/null +++ b/.github/workflows/ci-bcf-pypi.yml @@ -0,0 +1,57 @@ +name: ci-bcf-pypi + +on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # * * * * * + - cron: "0 0 18 * *" + push: + paths: + - '.github/workflows/ci-bcf-pypi.yml' + workflow_dispatch: + +env: + major: 0 + minor: 0 + name: ifcopenshell + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + name: ${{ matrix.config.name }}-${{ matrix.pyver }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 # https://github.com/actions/checkout + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + - run: echo ${{ env.DATE }} + - name: Get current date + id: date + run: echo "::set-output name=date::$(date +'%y%m%d')" + - name: Compile + run: | + pip install build + cd src/bcf && + python -m build + - name: Publish a Python distribution to PyPI + uses: ortega2247/pypi-upload-action@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: src/bcf/dist diff --git a/.github/workflows/ci-bcf.yml b/.github/workflows/ci-bcf.yml deleted file mode 100644 index 2d82036975..0000000000 --- a/.github/workflows/ci-bcf.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: ci-bcf - -on: - push: - -jobs: - activate: - runs-on: ubuntu-latest - if: | - github.repository == 'IfcOpenShell/IfcOpenShell' && - contains(github.event.head_commit.message, '[bcf release]') - steps: - - run: echo ok go - upload: - needs: activate - name: Upload BCF package to Pypi - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - name: Build package - run: | - cd src/bcf - pip install build - python -m build - - name: Test - run: | - cd src/bcf - make test - - name: Publish package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} - packages_dir: src/bcf/dist diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 3d8bf6b965..de25096754 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -146,7 +146,10 @@ classes = [ # Structural analysis ui.BIM_PT_tab_structural, # Construction scheduling - ui.BIM_PT_tab_4D5D, + ui.BIM_PT_tab_status, + ui.BIM_PT_tab_resources, + ui.BIM_PT_tab_cost, + ui.BIM_PT_tab_sequence, # Facility management ui.BIM_PT_tab_handover, ui.BIM_PT_tab_operations, diff --git a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc index ce61846389..5a2227b90e 100644 --- a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc +++ b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc @@ -5,27 +5,27 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset FILE_SCHEMA(('IFC4')); ENDSEC; DATA; -#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21)); +#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22)); #2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); -#4=IFCSIMPLEPROPERTYTEMPLATE('2T$a4OFsv2LeD5JeBKEV4f',$,'IsNTS','Whether or not the scale is intended to be significant',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#5=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#6=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#7=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#8=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#16=IFCSIMPLEPROPERTYTEMPLATE('1$xfo9EVb26QLqmPll2_RK',$,'MetricPrecision','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); -#17=IFCSIMPLEPROPERTYTEMPLATE('38uAtrp9nD_901NO42zd$7',$,'ImperialPrecision','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); -#18=IFCSIMPLEPROPERTYTEMPLATE('1MX0uffTL6TOvtvEJpxFmk',$,'DecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); -#19=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#20=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#21=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +#5=IFCSIMPLEPROPERTYTEMPLATE('2T$a4OFsv2LeD5JeBKEV4f',$,'IsNTS','Whether or not the scale is intended to be significant',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#16=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#17=IFCSIMPLEPROPERTYTEMPLATE('1$xfo9EVb26QLqmPll2_RK',$,'MetricPrecision','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#18=IFCSIMPLEPROPERTYTEMPLATE('38uAtrp9nD_901NO42zd$7',$,'ImperialPrecision','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +#19=IFCSIMPLEPROPERTYTEMPLATE('1MX0uffTL6TOvtvEJpxFmk',$,'DecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); +#20=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#21=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#22=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); ENDSEC; END-ISO-10303-21; diff --git a/src/blenderbim/blenderbim/bim/data/pset/EPset_Status.ifc b/src/blenderbim/blenderbim/bim/data/pset/EPset_Status.ifc new file mode 100644 index 0000000000..8b48798f75 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/pset/EPset_Status.ifc @@ -0,0 +1,13 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION((),'2;1'); +FILE_NAME('EPset_Status.ifc','2020-01-01T00:00:00',(),(),'EPset_Status','EPset_Status',$); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROPERTYSETTEMPLATE('3JoyTt$cz4fhYuQvJeTMhs',$,'EPset_Status','',.PSET_TYPEDRIVENOVERRIDE.,'IfcProduct',(#2,#4)); +#2= IFCSIMPLEPROPERTYTEMPLATE('2HBOdOfz5E3hZ7IBLCq$$D',$,'Status','Status of the element, predominately used in renovation or retrofitting projects. The status can be assigned to as "New" - element designed as new addition, "Existing" - element exists and remains, "Demolish" - element existed but is to be demolished, "Temporary" - element will exists only temporary (like a temporary support structure).',.P_ENUMERATEDVALUE.,'IfcLabel',$,#3,$,$,$,.READWRITE.); +#3= IFCPROPERTYENUMERATION('EPEnum_ElementStatus',(IFCLABEL('NEW'),IFCLABEL('EXISTING'),IFCLABEL('DEMOLISH'),IFCLABEL('TEMPORARY'),IFCLABEL('OTHER'),IFCLABEL('NOTKNOWN'),IFCLABEL('UNSET')),$); +#4=IFCSIMPLEPROPERTYTEMPLATE('3ttXxqmlz62BlxjzEPpqvH',$,'UserDefinedStatus','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +ENDSEC; +END-ISO-10303-21; diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 50412c2354..5813e8081a 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -82,7 +82,7 @@ class IfcExporter: ) self.file.wrapped_data.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version) self.file.wrapped_data.header.file_name.originating_system = "{} {}".format( - self.get_application_name(), self.get_application_version() + self.get_application_name(), tool.Blender.get_blenderbim_version() ) def sync_all_objects(self): @@ -162,7 +162,7 @@ class IfcExporter: return "BlenderBIM" def get_application_version(self): - return ".".join( + version = ".".join( [ str(x) for x in [ @@ -172,6 +172,9 @@ class IfcExporter: ][0] ] ) + if blenderbim.bim.last_commit_hash != "8888888": + version += f"-{blenderbim.bim.last_commit_hash[:7]}" + return version class IfcExportSettings: diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 27e9f34c7a..c8a2882475 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -27,6 +27,7 @@ from bpy.app.handlers import persistent from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation from blenderbim.bim.module.model.data import AuthoringData +from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP from mathutils import Vector from math import cos, degrees @@ -73,17 +74,27 @@ def name_callback(obj, data): refresh_ui_data() return - if not obj.BIMObjectProperties.ifc_definition_id or "/" not in obj.name: + if not obj.BIMObjectProperties.ifc_definition_id: return + element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + if "/" in obj.name: + object_name = obj.name + element_name = obj.name.split("/", 1)[1] + else: + element_name = obj.name + object_name = element.is_a() + f"/{element_name}" + obj.name = object_name # NOTE: doesn't trigger infinite recursion + if element.is_a("IfcGridAxis"): - element.AxisTag = obj.name.split("/")[1] + element.AxisTag = object_name.split("/")[1] refresh_ui_data() + if not element.is_a("IfcRoot"): return + element.Name = element_name if obj.BIMObjectProperties.collection: - obj.BIMObjectProperties.collection.name = obj.name - element.Name = "/".join(obj.name.split("/")[1:]) + obj.BIMObjectProperties.collection.name = object_name refresh_ui_data() @@ -106,7 +117,7 @@ def update_bim_tool_props(): return mode = bpy.context.mode current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode) - if not current_tool or current_tool.idname != "bim.bim_tool": + if not current_tool or current_tool.idname not in LIST_OF_TOOLS: return element = tool.Ifc.get_entity(obj) if not element: @@ -114,6 +125,16 @@ def update_bim_tool_props(): representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return + + props = bpy.context.scene.BIMModelProperties + if element.is_a("IfcElementType") or element.is_a("IfcElement"): + element_type = ifcopenshell.util.element.get_type(element) + if element_type: + is_bim_tool = current_tool.idname == "bim.bim_tool" + if is_bim_tool: + props.ifc_class = element_type.is_a() + if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a(): + props.relating_type_id = str(element_type.id()) extrusion = tool.Model.get_extrusion(representation) if not extrusion: return @@ -124,7 +145,6 @@ def update_bim_tool_props(): return x_angle si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - props = bpy.context.scene.BIMModelProperties if not AuthoringData.is_loaded: AuthoringData.load() diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 8b6ffe2a6b..aa2100b0c5 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -277,6 +277,8 @@ def draw_filter(layout, props, data, module): if not data.is_loaded: data.load() + sprops = bpy.context.scene.BIMSearchProperties + if tool.Ifc.get(): row = layout.row(align=True) row.label(text=f"{len(data.data['saved_searches'])} Saved Searches") @@ -293,9 +295,9 @@ def draw_filter(layout, props, data, module): box = layout.box() row = box.row(align=True) - row.prop(props, "facet", text="") + row.prop(sprops, "facet", text="") op = row.operator("bim.add_filter", text="Add Filter", icon="ADD") - op.type = props.facet + op.type = sprops.facet op.index = i op.module = module diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index 97d2f301b3..c35a942aaa 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -221,7 +221,9 @@ class IfcStore: blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) elif isinstance(obj, bpy.types.Object): blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) - blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback) + blenderbim.bim.handler.subscribe_to( + obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback + ) if IfcStore.history: data = {"id": element.id(), "guid": getattr(element, "GlobalId", None), "obj": obj.name} @@ -248,7 +250,9 @@ class IfcStore: blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) elif isinstance(obj, bpy.types.Object): blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) - blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback) + blenderbim.bim.handler.subscribe_to( + obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback + ) # TODO Listeners are not re-registered. Does this cause nasty problems to debug later on? # TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems. @@ -321,7 +325,7 @@ class IfcStore: IfcStore.begin_transaction(operator) if tool.Ifc.get(): tool.Ifc.get().begin_transaction() - if BrickStore.graph: + if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages. BrickStore.begin_transaction() # This empty transaction ensures that each operator has at least one transaction IfcStore.add_transaction_operation(operator, rollback=lambda data: True, commit=lambda data: True) @@ -339,7 +343,7 @@ class IfcStore: IfcStore.add_transaction_operation( operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo() ) - if BrickStore.graph: + if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages. BrickStore.end_transaction() IfcStore.end_transaction(operator) blenderbim.bim.handler.refresh_ui_data() diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index a6ce741550..1ed67ac988 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -227,16 +227,16 @@ class IfcImporter: self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance) self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance) self.settings.set(self.settings.STRICT_TOLERANCE, True) - self.settings_curve = ifcopenshell.geom.settings() - self.settings_curve.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance) - self.settings_curve.set_angular_tolerance(self.ifc_import_settings.angular_tolerance) - self.settings_curve.set(self.settings_curve.STRICT_TOLERANCE, True) - self.settings_curve.set(self.settings_curve.INCLUDE_CURVES, True) + self.settings_body_2d = ifcopenshell.geom.settings() + self.settings_body_2d.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance) + self.settings_body_2d.set_angular_tolerance(self.ifc_import_settings.angular_tolerance) + self.settings_body_2d.set(self.settings_body_2d.STRICT_TOLERANCE, True) + self.settings_body_2d.set(self.settings_body_2d.INCLUDE_CURVES, True) self.settings_native = ifcopenshell.geom.settings() self.settings_native.set(self.settings_native.INCLUDE_CURVES, True) - self.settings_2d = ifcopenshell.geom.settings() - self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True) - self.settings_2d.set(self.settings_2d.STRICT_TOLERANCE, True) + self.settings_plan_2d = ifcopenshell.geom.settings() + self.settings_plan_2d.set(self.settings_plan_2d.INCLUDE_CURVES, True) + self.settings_plan_2d.set(self.settings_plan_2d.STRICT_TOLERANCE, True) self.project = None self.has_existing_project = False self.collections = {} @@ -348,13 +348,14 @@ class IfcImporter: if c.ContextIdentifier in ["Body", "Facetation"] ] # Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly - self.body_contexts.extend( - [ - c.id() - for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False) - if c.ContextType == "Model" - ] - ) + if not self.body_contexts: + self.body_contexts.extend( + [ + c.id() + for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False) + if c.ContextType == "Model" + ] + ) if self.body_contexts: self.settings.set_context_ids(self.body_contexts) # Annotation ContextType is to accommodate broken Revit files @@ -365,7 +366,7 @@ class IfcImporter: if c.ContextType in ["Plan", "Annotation"] or c.ContextIdentifier == "Annotation" ] if self.plan_contexts: - self.settings_2d.set_context_ids(self.plan_contexts) + self.settings_plan_2d.set_context_ids(self.plan_contexts) def process_element_filter(self): offset = self.ifc_import_settings.element_offset @@ -645,7 +646,7 @@ class IfcImporter: self.ifc_import_settings.logger.error("An invalid grid was found %s", grid) continue if grid.Representation: - shape = ifcopenshell.geom.create_shape(self.settings_2d, grid) + shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, grid) grid_obj = self.create_product(grid, shape) if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import: grid_obj.lock_location = (True, True, True) @@ -664,7 +665,7 @@ class IfcImporter: def create_grid_axes(self, axes, grid_collection, grid_obj): for axis in axes: - shape = ifcopenshell.geom.create_shape(self.settings_2d, axis.AxisCurve) + shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, axis.AxisCurve) mesh = self.create_mesh(axis, shape) obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh) if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import: @@ -698,7 +699,7 @@ class IfcImporter: shape = ifcopenshell.geom.create_shape(self.settings, representation) except: try: - shape = ifcopenshell.geom.create_shape(self.settings_2d, representation) + shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, representation) except: self.ifc_import_settings.logger.error("Failed to generate shape for %s", element) if shape: @@ -764,9 +765,9 @@ class IfcImporter: if self.ifc_import_settings.should_load_geometry: products = self.create_products(elements) elements -= products - products = self.create_products(elements, settings=self.settings_curve) + products = self.create_products(elements, settings=self.settings_body_2d) elements -= products - products = self.create_products(elements, settings=self.settings_2d) + products = self.create_products(elements, settings=self.settings_plan_2d) elements -= products products = self.create_pointclouds(elements) elements -= products @@ -901,10 +902,10 @@ class IfcImporter: self.structural_collection.children.link(self.structural_connection_collection) self.project["blender"].children.link(self.structural_collection) - self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_2d) - self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_2d) - self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_2d) - self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_2d) + self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_plan_2d) + self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_plan_2d) + self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_plan_2d) + self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_plan_2d) self.create_structural_point_connections() def create_structural_point_connections(self): diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index ac53d3eb41..3908ee88a7 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -39,8 +39,12 @@ classes = ( prop.Brick, prop.BIMBrickProperties, ui.BIM_PT_brickschema, - ui.BIM_PT_ifc_brickschema_references, + ui.BIM_PT_brickschema_project_info, + ui.BIM_PT_brickschema_namespaces, + ui.BIM_PT_brickschema_create_entity, + ui.BIM_PT_brickschema_viewport, ui.BIM_UL_bricks, + ui.BIM_PT_ifc_brickschema_references, ) diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index b08e25280c..db8cb856d9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -46,7 +46,7 @@ class BrickschemaData: @classmethod def get_is_loaded(cls): - return BrickStore.graph is not None + return BrickStore.graph is not None # `if BrickStore.graph` by itself takes ages. @classmethod def active_relations(cls): @@ -67,13 +67,17 @@ class BrickschemaData: PREFIX brick: PREFIX rdfs: PREFIX rdf: - SELECT DISTINCT ?predicate ?object ?sp ?sv WHERE { - <{uri}> ?predicate ?object . - OPTIONAL { - { ?predicate rdfs:range brick:TimeseriesReference . } - UNION - { ?predicate a brick:EntityProperty . } - ?object ?sp ?sv } + SELECT DISTINCT ?predicate ?object ?label ?sp ?sv WHERE { + <{uri}> ?predicate ?object . + OPTIONAL { + ?object rdfs:label ?label . + } + OPTIONAL { + { ?predicate rdfs:range brick:TimeseriesReference . } + UNION + { ?predicate a brick:EntityProperty . } + ?object ?sp ?sv . + } } GROUP BY ?object """.replace( @@ -84,14 +88,15 @@ class BrickschemaData: predicate_uri = row.get("predicate") predicate_name = predicate_uri.toPython().split("#")[-1] object_uri = row.get("object") - print("DEBUG: object is ", object_uri) - if isinstance(object_uri, BNode): - object_name = "[]" - else: - try: - object_name = object_uri.toPython().split("#")[-1] - except: - object_name = str(object_uri) + object_name = row.get("label") + if not object_name: + if isinstance(object_uri, BNode): + object_name = "[]" + else: + try: + object_name = object_uri.toPython().split("#")[-1] + except: + object_name = str(object_uri) results.append( { "predicate_uri": predicate_uri, @@ -99,7 +104,7 @@ class BrickschemaData: "object_uri": object_uri, "object_name": object_name, "is_uri": isinstance(object_uri, URIRef), - "is_globalid": predicate_uri == "globalID", + "is_globalid": predicate_name == "ifcGlobalID", } ) if isinstance(object_uri, BNode): @@ -109,7 +114,6 @@ class BrickschemaData: object2_name = object2.toPython().split("#")[-1] except: object2_name = str(object2) - print("DEBUG: ", predicate_name, ":", predicate2_name) results.append( { "predicate_uri": None, @@ -117,10 +121,9 @@ class BrickschemaData: "object_uri": object2, "object_name": object2_name, "is_uri": isinstance(object2, URIRef), - "is_globalid": predicate2_name == "globalID", + "is_globalid": predicate2_name == "ifcGlobalID", } ) - print("") return results @@ -146,7 +149,7 @@ class BrickschemaReferencesData: for library in ifc.by_type("IfcLibraryInformation"): if tool.Ifc.get_schema() == "IFC2X3": results.append((str(library.id()), library.Name or "Unnamed", "")) - elif ".ttl" in library.Location: + elif library.Location and ".ttl" in library.Location: results.append((str(library.id()), library.Name or "Unnamed", "")) return results diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 88a25b90b4..2c57e937e5 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -105,6 +105,7 @@ class ConvertBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.convert_brick_project" bl_label = "Convert Brick Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create an Ifc library for this Brick project" def _execute(self, context): core.convert_brick_project(tool.Ifc, tool.Brick) @@ -114,9 +115,18 @@ class AssignBrickReference(bpy.types.Operator, Operator): bl_idname = "bim.assign_brick_reference" bl_label = "Assign Brick Reference" bl_options = {"REGISTER", "UNDO"} + bl_description = "Assign the selected Ifc entity to the selected Brick entity" def _execute(self, context): + if not context.active_object: + self.report({'ERROR'}, f'No Ifc selected') + return props = context.scene.BIMBrickProperties + try: + props.bricks[props.active_brick_index] + except: + self.report({'ERROR'}, f'No Brick selected') + return core.assign_brick_reference( tool.Ifc, tool.Brick, @@ -159,7 +169,7 @@ class AddBrickRelation(bpy.types.Operator, Operator): elif props.split_screen_toggled: object = props.split_screen_bricks[props.split_screen_active_brick_index].uri else: - object = props.new_brick_relation_namespace + props.new_brick_relation_object + object = props.namespace + props.new_brick_relation_object core.add_brick_relation( tool.Brick, brick_uri=brick.uri, @@ -172,6 +182,7 @@ class ConvertIfcToBrick(bpy.types.Operator, Operator): bl_idname = "bim.convert_ifc_to_brick" bl_label = "Convert IFC To Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Convert Ifc entities and relations to Brick entities and relations" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -222,7 +233,6 @@ class RefreshBrickViewer(bpy.types.Operator, Operator): def _execute(self, context): core.refresh_brick_viewer(tool.Brick) - core.refresh_brick_viewer(tool.Brick, split_screen=True) class RemoveBrick(bpy.types.Operator, Operator): @@ -278,7 +288,7 @@ class AddBrickNamespace(bpy.types.Operator, Operator): props = context.scene.BIMBrickProperties alias = props.new_brick_namespace_alias uri = props.new_brick_namespace_uri - core.add_namespace(tool.Brick, alias=alias, uri=uri) + core.add_brick_namespace(tool.Brick, alias=alias, uri=uri) class RemoveBrickRelation(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index fed515b044..a9f271e927 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -45,12 +45,12 @@ def get_libraries(self, context): def get_namespaces(self, context): - return BrickStore.namespaces + return [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces] def get_brick_entity_classes(self, context): entity = self.brick_entity_create_type - return BrickStore.entity_classes[entity] + return [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]] def get_brick_roots(self, context): @@ -58,13 +58,13 @@ def get_brick_roots(self, context): def get_brick_relations(self, context): - def is_label(relation): - return relation["predicate_name"] == "label" - if not list(filter(is_label, BrickschemaData.data["active_relations"])): - new_relations = BrickStore.relationships.copy() - new_relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", "")) - return new_relations - return BrickStore.relationships + relations = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships] + for relation in BrickschemaData.data["active_relations"]: + if relation["predicate_name"] == "label": + return relations + relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", "")) + return relations + def update_view(self, context): @@ -93,7 +93,6 @@ class BIMBrickProperties(PropertyGroup): 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) new_brick_namespace_alias: StringProperty(name="New Brick Namespace Alias") new_brick_namespace_uri: StringProperty(name="New Brick Namespace URI") # create brick entity @@ -104,9 +103,8 @@ class BIMBrickProperties(PropertyGroup): brick_create_relations_toggled: BoolProperty(name="Brick Create Relations Toggled", default=False) brick_edit_relations_toggled: BoolProperty(name="Brick Edit Relations Toggled", default=False) new_brick_relation_type: EnumProperty(name="New Brick Relation Type", items=get_brick_relations) - new_brick_relation_namespace: EnumProperty(name="New Brick Relation Namespace", items=get_namespaces) new_brick_relation_object: StringProperty(name="New Brick Relation Object") - add_relation_failed: BoolProperty(name="Add Relation Failed", default=False) + add_brick_relation_failed: BoolProperty(name="Add Relation Failed", default=False) # create relations split screen split_screen_toggled: BoolProperty(name="Split Screen Toggled", default=False) split_screen_bricks: CollectionProperty(name="Split Screen Bricks", type=Brick) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index a9b19d526d..b09507ea83 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -34,8 +34,19 @@ class BIM_PT_brickschema(Panel): def draw(self, context): if not BrickschemaData.is_loaded: BrickschemaData.load() - self.props = context.scene.BIMBrickProperties + +class BIM_PT_brickschema_project_info(Panel): + bl_label = "Project Info" + bl_idname = "BIM_PT_brickschema_project_info" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" + + def draw(self, context): + self.props = context.scene.BIMBrickProperties + if not BrickschemaData.data["is_loaded"]: row = self.layout.row(align=True) row.operator("bim.new_brick_file", text="Create Project") @@ -61,48 +72,94 @@ class BIM_PT_brickschema(Panel): op.should_save_as = True row.operator("bim.close_brick_project", text="", icon="CANCEL") - 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) - row.label(text="Active Namespace:") +class BIM_PT_brickschema_namespaces(Panel): + bl_label = "Namespaces" + bl_idname = "BIM_PT_brickschema_namespaces" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" - row = box.row(align=True) - prop_with_search(row, self.props, "namespace", text="") + @classmethod + def poll(cls, context): + return BrickStore.graph != None - row = box.row(align=True) - row.label(text="Bind New Namespace:") - - row = box.row(align=True) - row.prop(data=self.props, property="new_brick_namespace_alias", text="") - col = row.column() - col.alignment = "CENTER" - col.scale_x = 1.1 - col.label(text=":") - row.prop(data=self.props, property="new_brick_namespace_uri", text="") - row.operator("bim.add_brick_namespace", text="", icon="ADD") + def draw(self, context): + self.props = context.scene.BIMBrickProperties row = self.layout.row(align=True) - row.label(text="Create Entity:") + row.label(text="Active Namespace:") row = self.layout.row(align=True) - row.prop(data=self.props, property="brick_entity_create_type", text="") + prop_with_search(row, self.props, "namespace", text="") - # hide this if selected entity already has a reference + row = self.layout.row(align=True) + row.label(text="Bind New Namespace:") + + row = self.layout.row(align=True) + row.prop(data=self.props, property="new_brick_namespace_alias", text="") + col = row.column() + col.alignment = "CENTER" + col.scale_x = 1.1 + col.label(text=":") + row.prop(data=self.props, property="new_brick_namespace_uri", text="") + row.operator("bim.add_brick_namespace", text="", icon="ADD") + + +class BIM_PT_brickschema_create_entity(Panel): + bl_label = "Create Entity" + bl_idname = "BIM_PT_brickschema_create_entity" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" + + @classmethod + def poll(cls, context): + return BrickStore.graph != None + + def draw(self, context): + self.props = context.scene.BIMBrickProperties + # TO DO: hide this if selected entity already has a reference, or something similar + + row = self.layout.row(align=True) + row.prop(data=self.props, property="brick_entity_create_type", text="Class") + + row = self.layout.row(align=True) + prop_with_search(row, self.props, "brick_entity_class", text="Type") + row = self.layout.row(align=True) active = tool.Ifc.get_entity(context.active_object) if active and context.selected_objects: + row.label(text="Label:") row.label(text=active.Name if active.Name else "Unnamed") else: - 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.prop(data=self.props, property="new_brick_label", text="Label") row = self.layout.row(align=True) - col = row.column() - col.alignment = "RIGHT" + row.operator("bim.add_brick", text="Create Entity") + + +class BIM_PT_brickschema_viewport(Panel): + bl_label = "Viewport" + bl_idname = "BIM_PT_brickschema_viewport" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" + + @classmethod + def poll(cls, context): + return BrickStore.graph != None + + def draw(self, context): + self.props = context.scene.BIMBrickProperties + + row = self.layout.row(align=True) + row.column().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") @@ -141,54 +198,47 @@ class BIM_PT_brickschema(Panel): if BrickschemaData.data["active_relations"]: row = self.layout.row(align=True) - col = row.column() - col.alignment = "RIGHT" + row.column().alignment = "RIGHT" row.prop(data=self.props, property="brick_create_relations_toggled", text="", icon="PLUGIN") row.prop(data=self.props, property="brick_edit_relations_toggled", text="", icon="TOOL_SETTINGS") row.operator("bim.remove_brick", text="", icon="X") - if self.props.brick_create_relations_toggled: + if self.props.brick_create_relations_toggled and self.props.split_screen_toggled: row = self.layout.row(align=True) - row.label(text="Create Relation:") - - if self.props.brick_create_relations_toggled and self.props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label": - row = self.layout.row(align=True) - 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") - - elif self.props.brick_create_relations_toggled and self.props.split_screen_toggled: - row = self.layout.row(align=True) - split_screen_selection = self.props.split_screen_bricks[self.props.split_screen_active_brick_index] - if split_screen_selection.total_items: + try: + split_screen_selection = self.props.split_screen_bricks[self.props.split_screen_active_brick_index] + except: + split_screen_selection = None + if not split_screen_selection or split_screen_selection.total_items: row.label(text="No selection", icon="INFO") else: prop_with_search(row, self.props, "new_brick_relation_type", text="") row.label(text=split_screen_selection.label if split_screen_selection.label else split_screen_selection.name) row.operator("bim.add_brick_relation", text="", icon="ADD") - elif self.props.brick_create_relations_toggled: - row = self.layout.row(align=True) - prop_with_search(row, self.props, "new_brick_relation_namespace", text="") - + elif self.props.brick_create_relations_toggled: row = self.layout.row(align=True) 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: + if self.props.brick_create_relations_toggled and self.props.add_brick_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) + split = self.layout.split(factor=0.85, align=True) + row = split.row(align=True) row.label(text=relation["predicate_name"]) + row.separator() row.label(text=relation["object_name"]) + row = split.row(align=True) + row.column().alignment = "RIGHT" if self.props.brick_edit_relations_toggled and relation["predicate_uri"] and relation["predicate_name"] != "type": op = row.operator("bim.remove_brick_relation", text="", icon="UNLINKED") op.predicate = relation["predicate_uri"] op.object = relation["object_uri"] - if relation["is_uri"] and relation["object_name"] != self.props.active_brick_class: + if relation["is_uri"] and relation["object_uri"].toPython().split("#")[-1] != self.props.active_brick_class: op = row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT") op.item = relation["object_uri"] if relation["is_globalid"]: @@ -196,6 +246,23 @@ class BIM_PT_brickschema(Panel): op.global_id = relation["object_name"] +class BIM_UL_bricks(UIList): + split_screen = False + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + split = layout.split(factor=0.85, align=True) + row = split.row() + label = item.label if item.label else item.name + if item.total_items: + op = row.operator("bim.view_brick_class", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) + op.brick_class = item.name + op.split_screen = self.split_screen + row.label(text=label) + if item.total_items: + row = split.row() + row.label(text=str(item.total_items)) + class BIM_PT_ifc_brickschema_references(Panel): bl_label = "Brickschema References" bl_idname = "BIM_PT_ifc_brickschema_references" @@ -219,18 +286,24 @@ class BIM_PT_ifc_brickschema_references(Panel): row.label(text="No Brickschema Project Loaded") return - if not BrickschemaReferencesData.data["libraries"]: + if not BrickschemaReferencesData.data["libraries"] and BrickStore.path: row = self.layout.row(align=True) - if BrickStore.path: - row.label(text="No IFC Libraries") - row.operator("bim.convert_brick_project", text="", icon="ADD") - else: - row.label(text="No IFC Libraries. Save the Brick project to create a new library.", icon="ERROR") + row.label(text="No IFC Libraries") + row.operator("bim.convert_brick_project", text="", icon="ADD") + return + + elif not BrickschemaReferencesData.data["libraries"]: + row = self.layout.row(align=True) + row.label(text="No IFC Libraries: save the Brick project to create a new library", icon="ERROR") + + row = self.layout.row(align=True) + row.label(text="Libraries must have location pointing to a \".ttl\" file", icon="INFO") return row = self.layout.row(align=True) prop_with_search(row, self.props, "libraries") - row.operator("bim.convert_brick_project", text="", icon="ADD") + if BrickStore.path: + row.operator("bim.convert_brick_project", text="", icon="ADD") row = self.layout.row(align=True) row.operator("bim.assign_brick_reference", icon="ADD") @@ -245,19 +318,4 @@ class BIM_PT_ifc_brickschema_references(Panel): row.label(text=reference["identification"], icon="ASSET_MANAGER") row.label(text=reference["name"]) row.operator("bim.unassign_library_reference", text="", icon="X").reference = reference["id"] - row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"] - - -class BIM_UL_bricks(UIList): - split_screen = False - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - row = layout.row(align=True) - label = item.label if item.label else item.name - if item.total_items: - op = row.operator("bim.view_brick_class", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) - op.brick_class = item.name - op.split_screen = self.split_screen - label = label + " (" + str(item.total_items) + ")" - row.label(text=label) + row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"] \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 0d00b12a71..ae7f17da8c 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -29,7 +29,8 @@ class BIM_PT_cost_schedules(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_cost" + bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/csv/__init__.py b/src/blenderbim/blenderbim/bim/module/csv/__init__.py index 1f1502b695..b8cc7d0079 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/csv/__init__.py @@ -21,13 +21,14 @@ from . import ui, prop, operator classes = ( operator.AddCsvAttribute, - operator.RemoveCsvAttribute, - operator.RemoveAllCsvAttributes, - operator.ExportIfcCsv, - operator.ImportIfcCsv, - operator.SelectCsvIfcFile, - operator.ImportCsvAttributes, operator.ExportCsvAttributes, + operator.ExportIfcCsv, + operator.ImportCsvAttributes, + operator.ImportIfcCsv, + operator.RemoveAllCsvAttributes, + operator.RemoveCsvAttribute, + operator.ReorderCsvAttribute, + operator.SelectCsvIfcFile, prop.CsvAttribute, prop.CsvProperties, ui.BIM_PT_ifccsv, diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index ee3e312e29..c1b63e4dd3 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -62,6 +62,24 @@ class RemoveAllCsvAttributes(bpy.types.Operator): return {"FINISHED"} +class ReorderCsvAttribute(bpy.types.Operator): + bl_idname = "bim.reorder_csv_attribute" + bl_label = "Reorder CSV Attribute" + bl_options = {"REGISTER", "UNDO"} + old_index: bpy.props.IntProperty() + new_index: bpy.props.IntProperty() + + def execute(self, context): + old = context.scene.CsvProperties.csv_attributes[self.old_index] + new = context.scene.CsvProperties.csv_attributes[self.new_index] + props = ["name", "header", "sort", "group", "varies_value", "summary"] + for prop in props: + value = getattr(new, prop) + setattr(new, prop, getattr(old, prop)) + setattr(old, prop, value) + return {"FINISHED"} + + class ImportCsvAttributes(bpy.types.Operator): bl_idname = "bim.import_csv_attributes" bl_label = "Load CSV Settings" diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index 96fc51e822..31a334e389 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -38,24 +38,24 @@ class CsvAttribute(PropertyGroup): sort: EnumProperty(items=[("NONE", "None", ""), ("ASC", "Ascending", ""), ("DESC", "Descending", "")]) group: EnumProperty( items=[ - ("NONE", "None", ""), - ("GROUP", "Group", "All rows where this value is identical will be merged."), - ("CONCAT", "Concatenation", "Concatenate values if values vary within a group."), - ("VARIES", "Varies", "Show a custom value if values vary within a group."), - ("SUM", "Sum", "Sums the total value of rows in a group."), - ("AVERAGE", "Average", "Averages the total value of rows in a group."), - ("MIN", "Min", "Gets the minimum value of rows in a group."), - ("MAX", "Max", "Gets the maximum value of rows in a group."), + ("NONE", "None", "Don't group any rows"), + ("GROUP", "Group", "All rows where this value is identical will be merged"), + ("CONCAT", "Concatenation", "Concatenate values if values vary within a group"), + ("VARIES", "Varies", "Show a custom value if values vary within a group"), + ("SUM", "Sum", "Sums the total value of rows in a group"), + ("AVERAGE", "Average", "Averages the total value of rows in a group"), + ("MIN", "Min", "Gets the minimum value of rows in a group"), + ("MAX", "Max", "Gets the maximum value of rows in a group"), ] ) varies_value: StringProperty(default="Varies", name="Varies Value") summary: EnumProperty( items=[ - ("NONE", "None", ""), - ("SUM", "Sum", "Sums the total value of all rows."), - ("AVERAGE", "Average", "Averages the total value of all rows."), - ("MIN", "Min", "Gets the minimum value of all rows."), - ("MAX", "Max", "Gets the maximum value of all rows."), + ("NONE", "None", "Don't provide a summary row"), + ("SUM", "Sum", "Sums the total value of all rows"), + ("AVERAGE", "Average", "Averages the total value of all rows"), + ("MIN", "Min", "Gets the minimum value of all rows"), + ("MAX", "Max", "Gets the maximum value of all rows"), ] ) @@ -64,18 +64,6 @@ class CsvProperties(PropertyGroup): csv_ifc_file: StringProperty(default="", name="IFC File") ifc_selector: StringProperty(default="", name="IFC Selector") filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") - facet: EnumProperty( - items=[ - ("entity", "Class", "", "FILE_3D", 0), - ("attribute", "Attribute", "", "COPY_ID", 1), - ("property", "Property", "", "PROPERTIES", 2), - ("material", "Material", "", "MATERIAL", 3), - ("classification", "Classification", "", "OUTLINER", 4), - ("location", "Location", "", "PACKAGE", 5), - ("type", "Type", "", "FILE_VOLUME", 6), - ("instance", "GlobalId", "", "GRIP", 7), - ], - ) csv_attributes: CollectionProperty(name="CSV Attributes", type=CsvAttribute) should_preserve_existing: BoolProperty(default=False, name="Preserve Existing") include_global_id: BoolProperty(default=True, name="Include GlobalId") diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index d4f4951155..c1e5c93222 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -87,6 +87,7 @@ class BIM_PT_ifccsv(Panel): row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="") row.prop(props, "should_show_summary", icon="SYNTAX_ON", text="") + total = len(props.csv_attributes) for index, attribute in enumerate(props.csv_attributes): row = layout.row(align=True) row.prop(attribute, "name", text="") @@ -99,6 +100,15 @@ class BIM_PT_ifccsv(Panel): row.prop(attribute, "varies_value", text="") if props.should_show_summary: row.prop(attribute, "summary", text="") + if total > 1: + if index != 0: + op = row.operator(f"bim.reorder_csv_attribute", icon="TRIA_UP", text="") + op.old_index = index + op.new_index = index - 1 + if index + 1 != total: + op = row.operator(f"bim.reorder_csv_attribute", icon="TRIA_DOWN", text="") + op.old_index = index + op.new_index = index + 1 row.operator("bim.remove_csv_attribute", icon="X", text="").index = index row = layout.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/data.py b/src/blenderbim/blenderbim/bim/module/geometry/data.py index 8b1849b200..22575778d3 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/data.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/data.py @@ -97,7 +97,7 @@ class ConnectionsData: @classmethod def load(cls): - cls.data = {"connections": cls.connections()} + cls.data = {"connections": cls.connections(), "is_connection_realization": cls.is_connection_realization()} cls.is_loaded = True @classmethod @@ -114,7 +114,13 @@ class ConnectionsData: else: related_element = rel.RelatedElement - if element.is_a("IfcRelConnectsPathElements"): + realizing_elements = [] + realizing_elements_connection_type = "" + if rel.is_a("IfcRelConnectsWithRealizingElements"): + realizing_elements.extend(rel.RealizingElements) + realizing_elements_connection_type = rel.ConnectionType + + if rel.is_a("IfcRelConnectsPathElements"): related_element_connection_type = rel.RelatedConnectionType else: related_element_connection_type = "" @@ -125,6 +131,8 @@ class ConnectionsData: "is_relating": True, "Name": related_element.Name or "Unnamed", "ConnectionType": related_element_connection_type, + "realizing_elements": realizing_elements, + "realizing_elements_connection_type": realizing_elements_connection_type, } ) @@ -134,7 +142,13 @@ class ConnectionsData: else: relating_element = rel.RelatingElement - if element.is_a("IfcRelConnectsPathElements"): + realizing_elements = [] + realizing_elements_connection_type = "" + if rel.is_a("IfcRelConnectsWithRealizingElements"): + realizing_elements.extend(rel.RealizingElements) + realizing_elements_connection_type = rel.ConnectionType + + if rel.is_a("IfcRelConnectsPathElements"): relating_element_connection_type = rel.RelatingConnectionType else: relating_element_connection_type = "" @@ -145,11 +159,30 @@ class ConnectionsData: "is_relating": False, "Name": relating_element.Name or "Unnamed", "ConnectionType": relating_element_connection_type, + "realizing_elements": realizing_elements, + "realizing_elements_connection_type": realizing_elements_connection_type, } ) return results + @classmethod + def is_connection_realization(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + connections = element.IsConnectionRealization + if not connections: + return + + results = [] + for rel in connections: + data = { + "realizing_elements_connection_type": rel.ConnectionType, + "connected_from": rel.RelatingElement, + "connected_to": rel.RelatedElement, + } + results.append(data) + return results + class DerivedPlacementsData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index c4ee14f969..226162559f 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -329,7 +329,7 @@ class UpdateRepresentation(bpy.types.Operator, Operator): "style.assign_representation_styles", self.file, shape_representation=new_representation, - styles=tool.Geometry.get_styles(obj), + styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True), should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, ) tool.Geometry.record_object_materials(obj) @@ -397,6 +397,8 @@ class GetRepresentationIfcParameters(bpy.types.Operator, Operator): def _execute(self, context): core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object) + parameters = context.active_object.data.BIMMeshProperties.ifc_parameters + self.report({"INFO"}, f"{len(parameters)} parameters found.") class CopyRepresentation(bpy.types.Operator, Operator): @@ -1370,8 +1372,13 @@ class OverrideModeSetEdit(bpy.types.Operator): continue is_profile = True + representation_class = tool.Geometry.get_ifc_representation_class(element, representation) if usage_type == "PROFILE": operator = lambda: bpy.ops.bim.hotkey(hotkey="A_E") + elif representation_class and "IfcCircleProfileDef" in representation_class: + self.report({"INFO"}, "Can't edit Circle Profile Extrusion") + obj.select_set(False) + continue elif ( tool.Geometry.is_profile_based(obj.data) or usage_type == "LAYER3" @@ -1418,8 +1425,7 @@ class OverrideModeSetEdit(bpy.types.Operator): else: obj.select_set(False) continue - - if not context.selected_objects or len(context.selected_objects) != len(selected_objs): + if len(selected_objs) > 1 and (not context.selected_objects or len(context.selected_objects) != len(selected_objs)): # We are trying to edit at least one non-mesh-like object : Display a hint to the user self.report({"INFO"}, "Only mesh-compatible representations may be edited concurrently in edit mode.") diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index ee18b60875..92a8167443 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -134,7 +134,7 @@ class BIM_PT_connections(Panel): layout = self.layout props = context.active_object.BIMObjectProperties - if not ConnectionsData.data["connections"]: + if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]: layout.label(text="No connections found") for connection in ConnectionsData.data["connections"]: @@ -146,6 +146,44 @@ class BIM_PT_connections(Panel): op = row.operator("bim.remove_connection", icon="X", text="") op.connection = connection["id"] + if connection["realizing_elements"]: + row = self.layout.row(align=True) + connection_type = connection["realizing_elements_connection_type"] + connection_type = f" ({connection_type})" if connection_type else "" + row.label(text=f"Realizing elements{connection_type}:") + + for element in connection["realizing_elements"]: + row = self.layout.row(align=True) + obj = tool.Ifc.get_object(element) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = element.id() + row.label(text=obj.name) + + # display connections where element is connection realization + connections = ConnectionsData.data["is_connection_realization"] + if not connections: + return + + row = self.layout.row(align=True) + row.label(text="Element is connections realization:") + for connection in connections: + # NOTE: not displayed yet + connection_type = connection["realizing_elements_connection_type"] + connection_type = f" ({connection_type})" if connection_type else "" + + row = self.layout.row(align=True) + + connected_from = connection["connected_from"] + obj = tool.Ifc.get_object(connected_from) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_from.id() + row.label(text=obj.name) + + row.label(text="", icon="FORWARD") + + connected_to = connection["connected_to"] + obj = tool.Ifc.get_object(connected_to) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_to.id() + row.label(text=obj.name) + class BIM_PT_mesh(Panel): bl_label = "Representation Utilities" diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index a0282a9a24..75681f9166 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -688,7 +688,13 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): ) if p not in (start_point, end_point) ] - entire_length = (first_segment_start - second_segment_end).length + + def get_segments_length(): + start_dir = (start_point - first_segment_start).normalized() + segments_vector = second_segment_end - first_segment_start + return segments_vector.dot(start_dir) + + entire_length = get_segments_length() # can't rely on (end_point-start_point) here because # transition might change the segments length and therefore direction will be changed @@ -717,28 +723,40 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): f"Failed to add transition - transition length is larger the segments and the distance between them.\n" + f"Transition length: {full_transition_length:.2f}m, segments length: {entire_length:.2f}m", ) - # TODO: handle the case without creating a representation in the first place? ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep) return {"CANCELLED"} + # calculate bunch of points to for adjustments middle_point = keep_only_z_axis((start_point + end_point) / 2 - start_point) + start_point start_segment_extend_point = middle_point - segments_dir * full_transition_length / 2 end_segment_extend_point = middle_point + segments_dir * full_transition_length / 2 + profile_offset_ws transition_dir = keep_only_z_axis(end_segment_extend_point - start_segment_extend_point).normalized() - DumbProfileJoiner().join_E(start_object, start_segment_extend_point) - DumbProfileJoiner().join_E(end_object, end_segment_extend_point) + # adjust the segments + end_object_rotation = end_object.matrix_world.to_quaternion() + end_object_z_basis = end_object_rotation.to_matrix().col[2] # z basis vector + if tool.Cad.is_x(start_object_z_basis.dot(transition_dir), 1): + start_connection = "ATEND" + else: + start_connection = "ATSTART" + if tool.Cad.is_x(end_object_z_basis.dot(transition_dir), 1): + end_connection = "ATSTART" + else: + end_connection = "ATEND" + DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection) + DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection) + + # find the compatible fitting type 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 if transition_type: # TODO: handle the case without creating a representation in the first place? ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep) - start_port_match = fitting_data["start_port_match"] if fitting_data else True + # create new fitting type if nothing is compatible if not transition_type: mesh = bpy.data.meshes.new("Transition") obj = bpy.data.objects.new("Transition", mesh) @@ -762,23 +780,28 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): ) # NOTE: at this point we loose current blender objects selection + # create transition element 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 + # required since we'll base our `transition_obj_dir` on this transition_obj.matrix_world = start_object.matrix_world context.view_layer.update() + # depending on transition direction we may need to flip it or attach it's origin to end segment + # direction can be different depending on: + # - order of the current segments + # - order of the segments that were used with the same transition type before transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj)) direction_match = tool.Cad.are_vectors_equal(transition_dir, transition_obj_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) + ports = tool.System.add_ports(transition_obj, offset_end_port=profile_offset_ws) 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") diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index ebbc764c79..714f88f73f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -70,7 +70,10 @@ class FilledOpeningGenerator: ) if target is None: - target = bpy.context.scene.cursor.location + should_set_z_level = True + target = bpy.context.scene.cursor.location.copy() + else: + should_set_z_level = False # Sometimes, the voided_obj may be an aggregate, which won't have any representation. if voided_obj.data: @@ -86,12 +89,17 @@ class FilledOpeningGenerator: axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"] new_matrix = voided_obj.matrix_world.copy() - new_matrix.translation = tool.Cad.point_on_edge(target, axis) + point_on_axis = tool.Cad.point_on_edge(target, axis) + new_matrix.translation.x = point_on_axis.x + new_matrix.translation.y = point_on_axis.y - if filling.is_a("IfcDoor"): - new_matrix.translation.z = voided_obj.matrix_world.translation.z + if should_set_z_level: + if filling.is_a("IfcDoor"): + new_matrix.translation.z = voided_obj.matrix_world.translation.z + else: + new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl2 else: - new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl2 + new_matrix.translation.z = filling_obj.matrix_world.copy().translation.z filling_obj.matrix_world = new_matrix bpy.context.view_layer.update() @@ -498,10 +506,14 @@ class AddBoolean(Operator, tool.Ifc.Operator): bl_idname = "bim.add_boolean" bl_label = "Add Boolean" bl_options = {"REGISTER", "UNDO"} + bl_description = "Applies a boolean to the selected IFC object using the other selected blender object as a void" @classmethod def poll(cls, context): - return len(context.selected_objects) == 2 + if not len(context.selected_objects) == 2: + cls.poll_message_set("Exactly 2 objects need to be selected.") + return False + return True def _execute(self, context): props = context.scene.BIMModelProperties @@ -646,6 +658,7 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): + upstream_obj = None for obj in context.selected_objects: if ( not obj.data @@ -673,6 +686,8 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): should_sync_changes_first=False, ) bpy.data.objects.remove(obj) + + tool.Blender.set_active_object(upstream_obj) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 10aa597da4..8bac8d9199 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -272,13 +272,21 @@ class DumbProfileJoiner: body = copy.deepcopy(axis1) self.recreate_profile(element1, profile1, axis, body) - def join_E(self, profile1, target): + def join_E(self, profile1, target, connection=None): + """`connection` = `ATEND` / `ATSTART` to explicitly define the reference point for the join. + + For example if profile 1m long and `target` is at (0, 0, 0.1) and `connection` = `None` + it will implicitly use `connection` = `ATSTART` resulting in profile object 0.9m long and moved to (0, 0, 0.1). + + But with `connection` = `ATEND` it will result in the profile object 0.1m long, locaiton unchanged. + """ element1 = tool.Ifc.get_entity(profile1) if not element1: return axis1 = self.get_profile_axis(profile1) - intersect, connection = mathutils.geometry.intersect_point_line(target, *axis1) - connection = "ATEND" if connection > 0.5 else "ATSTART" + intersect, connection_value = mathutils.geometry.intersect_point_line(target, *axis1) + if connection is None: + connection = "ATEND" if connection_value > 0.5 else "ATSTART" ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection) diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 8d2b66ce4d..ba0ac78390 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -78,9 +78,11 @@ class WallTool(BimTool): bl_description = "Create and edit walls" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.wall") bl_widget = None + ifc_element_type = "IfcWallType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcWallType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class SlabTool(BimTool): @@ -91,9 +93,11 @@ class SlabTool(BimTool): bl_description = "Create and edit slabs" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.slab") bl_widget = None + ifc_element_type = "IfcSlabType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcSlabType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class DoorTool(BimTool): @@ -104,9 +108,11 @@ class DoorTool(BimTool): bl_description = "Create and edit doors" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.door") bl_widget = None + ifc_element_type = "IfcDoorType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcDoorType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class WindowTool(BimTool): @@ -117,9 +123,11 @@ class WindowTool(BimTool): bl_description = "Create and edit windows" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.window") bl_widget = None + ifc_element_type = "IfcWindowType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcWindowType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class ColumnTool(BimTool): @@ -130,9 +138,11 @@ class ColumnTool(BimTool): bl_description = "Create and edit columns" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.column") bl_widget = None + ifc_element_type = "IfcColumnType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcColumnType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class BeamTool(BimTool): @@ -143,9 +153,11 @@ class BeamTool(BimTool): bl_description = "Create and edit beams" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.beam") bl_widget = None + ifc_element_type = "IfcBeamType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcBeamType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class DuctTool(BimTool): @@ -156,9 +168,11 @@ class DuctTool(BimTool): bl_description = "Create and edit ducks" # No, not a typo. bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.duct") bl_widget = None + ifc_element_type = "IfcDuctSegmentType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcDuctSegmentType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) class PipeTool(BimTool): @@ -169,9 +183,11 @@ class PipeTool(BimTool): bl_description = "Create and edit pipes" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.pipe") bl_widget = None + ifc_element_type = "IfcPipeSegmentType" - def draw_settings(context, layout, ws_tool): - BimToolUI.draw(context, layout, ifc_element_type="IfcPipeSegmentType") + @classmethod + def draw_settings(cls, context, layout, ws_tool): + BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) def add_layout_hotkey_operator(layout, text, hotkey, description): @@ -394,9 +410,7 @@ class BimToolUI: else: row.operator("bim.show_openings", icon="HIDE_OFF", text="") - if AuthoringData.data["active_class"] in ( - "IfcOpeningElement", - ): + if AuthoringData.data["active_class"] in ("IfcOpeningElement",): if len(context.selected_objects) == 2: row = cls.layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -737,9 +751,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): self.props.z = self.z def hotkey_S_L(self): - if AuthoringData.data["active_class"] in ( - "IfcOpeningElement", - ): + if AuthoringData.data["active_class"] in ("IfcOpeningElement",): if len(bpy.context.selected_objects) == 2: bpy.ops.bim.clone_opening() @@ -761,3 +773,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.edit_openings() else: bpy.ops.bim.show_openings() + + +LIST_OF_TOOLS = [cls.bl_idname for cls in (BimTool.__subclasses__() + [BimTool])] +TOOLS_TO_CLASSES_MAP = {cls.bl_idname: cls.ifc_element_type for cls in BimTool.__subclasses__()} diff --git a/src/blenderbim/blenderbim/bim/module/profile/data.py b/src/blenderbim/blenderbim/bim/module/profile/data.py index 89103cc0a4..9d74d2203c 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/data.py +++ b/src/blenderbim/blenderbim/bim/module/profile/data.py @@ -50,6 +50,8 @@ class ProfileData: @classmethod def active_profile_users(cls): profiles_props = bpy.context.scene.BIMProfileProperties + if profiles_props.active_profile_index >= len(profiles_props.profiles): + return 0 profile_prop = profiles_props.profiles[profiles_props.active_profile_index] profile_ifc = tool.Ifc.get().by_id(profile_prop.ifc_definition_id) return tool.Ifc.get().get_total_inverses(profile_ifc) diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index 293d46b4f9..adbb9cfbcd 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -455,7 +455,13 @@ class CopyPropertyToSelection(bpy.types.Operator, Operator): else: is_pset = context.active_object.PsetProperties.active_pset_type == "PSET" pset_name = context.active_object.PsetProperties.active_pset_name - prop_value = context.active_object.PsetProperties.properties.get(self.name).metadata.get_value() + prop = context.active_object.PsetProperties.properties.get(self.name) + if prop.value_type == "IfcPropertySingleValue": + prop_value = prop.metadata.get_value() + elif prop.value_type == "IfcPropertyEnumeratedValue": + value_name = prop.metadata.get_value_name() + prop_value = [e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected] + for obj in tool.Blender.get_selected_objects(): core.copy_property_to_selection( tool.Ifc, diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index 91b1d4751e..b3296530d4 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -71,6 +71,9 @@ def draw_enumerated_property(prop, layout, copy_operator=None): grid = layout.column_flow(columns=3) for e in prop.enumerated_value.enumerated_values: grid.prop(e, "is_selected", text=str(e[value_name])) + if copy_operator: + op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN") + op.name = prop.metadata.name def get_active_pset_obj_name(context, obj_type): diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index c91d7b7591..8512a370dd 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -20,36 +20,41 @@ import bpy from . import ui, prop, operator classes = ( - operator.DisableResourceEditingUI, - operator.DisableEditingResource, - operator.EnableEditingResource, - operator.LoadResources, operator.AddResource, operator.AddResourceQuantity, - operator.EditResource, - operator.RemoveResource, - operator.RemoveResourceQuantity, - operator.LoadResourceProperties, - operator.ExpandResource, - operator.ContractResource, + operator.AddProductivityData, operator.AssignResource, - operator.UnassignResource, - operator.EnableEditingResourceTime, - operator.EnableEditingResourceQuantity, - operator.EnableEditingResourceBaseQuantity, - operator.EnableEditingResourceCosts, - operator.EnableEditingResourceCostValueFormula, - operator.EnableEditingResourceCostValue, - operator.EditResourceTime, - operator.EditResourceQuantity, + operator.CalculateResourceWork, + operator.ConstrainResourceWork, + operator.ContractResource, + operator.DisableEditingResource, + operator.DisableEditingResourceCostValue, + operator.DisableEditingResourceQuantity, + operator.DisableEditingResourceTime, + operator.DisableResourceEditingUI, + operator.EditProductivityData, + operator.EditResource, operator.EditResourceCostValue, operator.EditResourceCostValueFormula, - operator.DisableEditingResourceTime, - operator.DisableEditingResourceQuantity, - operator.DisableEditingResourceCostValue, - operator.CalculateResourceWork, + operator.EditResourceQuantity, + operator.EditResourceTime, + operator.EnableEditingResource, + operator.EnableEditingResourceBaseQuantity, + operator.EnableEditingResourceCosts, + operator.EnableEditingResourceCostValue, + operator.CalculateResourceUsage, + operator.EnableEditingResourceCostValueFormula, + operator.EnableEditingResourceQuantity, + operator.EnableEditingResourceTime, + operator.ExpandResource, + operator.GoToResource, operator.ImportResources, - operator.EditProductivityData, + operator.LoadResourceProperties, + operator.LoadResources, + operator.RemoveResource, + operator.RemoveResourceQuantity, + operator.RemoveUsageConstraint, + operator.UnassignResource, prop.Resource, prop.BIMResourceProperties, prop.BIMResourceTreeProperties, diff --git a/src/blenderbim/blenderbim/bim/module/resource/data.py b/src/blenderbim/blenderbim/bim/module/resource/data.py index f9abe1729f..e70b29fb7a 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/data.py +++ b/src/blenderbim/blenderbim/bim/module/resource/data.py @@ -56,21 +56,75 @@ class ResourceData: "type": resource.is_a(), "BaseQuantity": base_quantity, } + results[resource.id()]["Benchmarks"] = cls.get_resource_benchmarks(resource) if resource.is_a() in ["IfcLaborResource", "IfcConstructionEquipmentResource"]: results[resource.id()]["Productivity"] = {} + results[resource.id()]["InheritedProductivity"] = {} productivity = cls.get_productivity(resource) if productivity: results[resource.id()]["Productivity"] = { + "id": productivity.get("id"), "QuantityProduced": ifcopenshell.util.resource.get_quantity_produced(productivity), "TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(productivity), "QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name(productivity), } + inherited_productivity = cls.get_parent_productivity(resource) + if inherited_productivity: + results[resource.id()]["InheritedProductivity"] = { + "QuantityProduced": ifcopenshell.util.resource.get_quantity_produced(inherited_productivity), + "TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(inherited_productivity), + "QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name( + inherited_productivity + ), + } + if resource.Usage: + results[resource.id()]["ScheduleWork"] = ( + ifcopenshell.util.date.readable_ifc_duration(resource.Usage.ScheduleWork) + if resource.Usage.ScheduleWork + else None + ) + results[resource.id()]["ScheduleUsage"] = ( + resource.Usage.ScheduleUsage if resource.Usage.ScheduleUsage else None + ) + if resource.IsNestedBy: + results[resource.id()]["DerivedScheduleWork"] = cls.sum_person_hours(resource) return results + @classmethod + def sum_person_hours(cls, resource): + sum = 0 + nested_resources = ifcopenshell.util.resource.get_nested_resources(resource) + for nested_resource in nested_resources or []: + if not nested_resource.Usage or not nested_resource.Usage.ScheduleWork: + continue + duration = ifcopenshell.util.date.ifc2datetime(nested_resource.Usage.ScheduleWork) + sum += duration.total_seconds() / 3600 + return round(float(sum), 2) if sum else 0 + + @classmethod + def get_resource_benchmarks(cls, resource): + constraints = [] + for constraint in ifcopenshell.util.constraint.get_constraints(resource) or []: + metrics = [] + for metric in ifcopenshell.util.constraint.get_metrics(constraint) or []: + metrics.append( + { + "reference": ifcopenshell.util.constraint.get_metric_reference(metric), + "Benchmark": metric.Benchmark, + "ConstraintGrade": metric.ConstraintGrade, + } + ) + constraints.append({"ObjectiveQualifier": constraint.ObjectiveQualifier, "metrics": metrics}) + return constraints + @classmethod def get_productivity(cls, resource): return ifcopenshell.util.resource.get_productivity(resource, should_inherit=False) + @classmethod + def get_parent_productivity(cls, resource): + return ifcopenshell.util.resource.get_parent_productivity(resource) + @classmethod def cost_values(cls): results = [] diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 9d9d0aeb07..fbdd4374f7 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -18,6 +18,7 @@ import bpy from bpy_extras.io_utils import ImportHelper +from blenderbim.bim.module.resource.ui import draw_productivity_ui import blenderbim.core.resource as core import blenderbim.tool as tool @@ -198,6 +199,13 @@ class CalculateResourceWork(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() + @classmethod + def poll(cls, context): + active_resource = tool.Resource.get_highlighted_resource() + if active_resource: + if tool.Resource.get_productivity(active_resource, should_inherit=True): + return True + def _execute(self, context): core.calculate_resource_work(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) @@ -348,6 +356,16 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): core.import_resources(tool.Resource, file_path=self.filepath) +class AddProductivityData(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_productivity_data" + bl_description = "Apply" + bl_label = "Add Productivity" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + tool.Ifc.run("pset.add_pset", product=tool.Resource.get_highlighted_resource(), name="EPset_Productivity") + + class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_productivity_data" bl_description = "Apply" @@ -356,3 +374,67 @@ class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.edit_productivity_pset(tool.Ifc, tool.Resource) + + def draw(self, context): + draw_productivity_ui(self, context) + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=600) + + +class ConstrainResourceWork(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_usage_constraint" + bl_label = "Constrain Resource Work" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + attribute: bpy.props.StringProperty() + + def _execute(self, context): + core.add_usage_constraint( + tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource), reference_path=self.attribute + ) + + +class RemoveUsageConstraint(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_usage_constraint" + bl_label = "Remove Usage Constraint" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + attribute: bpy.props.StringProperty() + + def _execute(self, context): + core.remove_usage_constraint( + tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource), reference_path=self.attribute + ) + + +class GoToResource(bpy.types.Operator): + bl_idname = "bim.go_to_resource" + bl_label = "Go To Resource" + bl_description = "Selects the resource in the Resource Panel" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + core.go_to_resource(tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) + return {"FINISHED"} + + +class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.calculate_resource_usage" + bl_label = "Calculate Resource Usage" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + active_resource = tool.Resource.get_highlighted_resource() + if active_resource: + if active_resource.Usage and active_resource.Usage.ScheduleWork: + task = tool.Resource.get_task_assignments(active_resource) + if task and tool.Sequence.has_duration(task): + return True + + return False + + def _execute(self, context): + core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(tool.Resource.get_highlighted_resource())) diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index 951ea9286c..9ea20d446c 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -1,5 +1,5 @@ # BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2021, 2022, 2023 Dion Moult, Yassine Oualid # # This file is part of BlenderBIM Add-on. # @@ -22,6 +22,8 @@ import ifcopenshell.util.resource from blenderbim.bim.ifc import IfcStore import blenderbim.tool as tool import blenderbim.bim.module.pset.data +import blenderbim.bim.module.resource.data +import blenderbim.bim.module.sequence.data from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -56,6 +58,8 @@ def updateResourceName(self, context): if props.active_resource_id == self.ifc_definition_id: attribute = props.resource_attributes.get("Name") attribute.string_value = self.name + blenderbim.bim.module.resource.data.refresh() + tool.Sequence.refresh_task_resources() def get_quantity_types(self, context): @@ -72,7 +76,7 @@ def get_quantity_types(self, context): def update_active_resource_index(self, context): blenderbim.bim.module.pset.data.refresh() - if self.should_show_productivity: + if self.should_show_resource_tools: tool.Resource.load_productivity_data() @@ -80,18 +84,20 @@ def updateResourceUsage(self, context): props = context.scene.BIMResourceProperties if not props.is_resource_update_enabled: return - - if self.schedule_usage == "": + if not self.schedule_usage: return resource = tool.Ifc.get().by_id(self.ifc_definition_id) - if not resource.Usage: - tool.Ifc.run( - "resource.add_resource_time", - resource=resource, - ) - resource.Usage.ScheduleUsage = self.schedule_usage - blenderbim.bim.module.pset.data.refresh() + if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage: + return + tool.Resource.run_edit_resource_time(resource, attributes={ + "ScheduleUsage": self.schedule_usage + }) + tool.Sequence.load_task_properties() tool.Resource.load_resource_properties() + tool.Sequence.refresh_task_resources() + blenderbim.bim.module.resource.data.refresh() + blenderbim.bim.module.sequence.data.refresh() + blenderbim.bim.module.pset.data.refresh() class ISODuration(PropertyGroup): @@ -144,7 +150,7 @@ class BIMResourceProperties(PropertyGroup): quantity_types: EnumProperty(items=get_quantity_types, name="Quantity Types") is_editing_quantity: BoolProperty(name="Is Editing Quantity") quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute) - should_show_productivity: BoolProperty(name="Edit Productivity", update=update_active_resource_index) + should_show_resource_tools: BoolProperty(name="Edit Productivity", update=update_active_resource_index) class BIMResourceProductivity(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 090aa0f8c4..29cb56aa07 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -28,7 +28,8 @@ class BIM_PT_resources(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_resources" + bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): @@ -64,58 +65,124 @@ class BIM_PT_resources(Panel): self.props, "active_resource_index", ) - row = self.layout.row(align=True) - row.alignment = "RIGHT" - row.prop(self.props, "should_show_productivity", icon="RECOVER_LAST") - if self.props.should_show_productivity: - self.draw_productivity_ui(context) - if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES": - self.draw_editable_resource_attributes_ui() - elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY": - self.draw_editable_resource_quantity_ui() - elif self.props.active_resource_id and self.props.editing_resource_type == "COSTS": - self.draw_editable_resource_costs_ui() - elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE": - self.draw_editable_resource_time_attributes_ui() + + if self.props.active_resource_id: + if self.props.editing_resource_type == "ATTRIBUTES": + self.draw_editable_resource_attributes_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY": + self.draw_editable_resource_quantity_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "COSTS": + self.draw_editable_resource_costs_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE": + self.draw_editable_resource_time_attributes_ui() + self.draw_productivity_ui(context) def draw_productivity_ui(self, context): - total_resources = len(self.tprops.resources) - if not total_resources or self.props.active_resource_index >= total_resources: - return - - ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id - resource = ResourceData.data["resources"][ifc_definition_id] - - if not resource["type"] in ["IfcConstructionEquipmentResource", "IfcLaborResource"]: - row = self.layout.row(align=True) - row.label(text="Resource type cannot have productivity data", icon="ERROR") - return - - self.productivity_props = context.scene.BIMResourceProductivity - - if resource["Productivity"]: - produtivitiy_rate_message = "Current Rate: {}/{}".format( - resource["Productivity"]["QuantityProduced"], resource["Productivity"]["TimeConsumed"] - ) - row = self.layout.row() - row.alignment = "LEFT" - row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") - else: - row = self.layout.row(align=True) - row.alignment = "LEFT" - produtivitiy_rate_message = "No productivity data found" - row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") - row = self.layout.row(align=True) row.alignment = "RIGHT" - row.prop(self.productivity_props, "quantity_produced", text="Quantity Produced") - row.prop(self.productivity_props, "quantity_produced_name", text="Quantity Name") - row = self.layout.row() - row.alignment = "RIGHT" - self.draw_duration_property(self.productivity_props.quantity_consumed, row) - row = self.layout.row() - row.alignment = "RIGHT" - row.operator("bim.edit_productivity_data", text="Apply", icon="CHECKMARK") + row.prop(self.props, "should_show_resource_tools", text="Resource Tools", icon="RECOVER_LAST") + if self.props.should_show_resource_tools: + total_resources = len(self.tprops.resources) + if not total_resources or self.props.active_resource_index >= total_resources: + return + + ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id + resource = ResourceData.data["resources"][ifc_definition_id] + + if not resource["type"] in ["IfcConstructionEquipmentResource", "IfcLaborResource"]: + row = self.layout.row(align=True) + row.label(text="Resource type cannot have productivity data", icon="ERROR") + else: + is_usage_locked = False + is_work_locked = False + for constraint in resource["Benchmarks"] or []: + for metric in constraint["metrics"] or []: + if ( + metric["ConstraintGrade"] == "HARD" + and metric["reference"] + and metric["reference"] == "Usage.ScheduleUsage" + ): + is_usage_locked = True + elif ( + metric["ConstraintGrade"] == "HARD" + and metric["reference"] + and metric["reference"] == "Usage.ScheduleWork" + ): + is_work_locked = True + grid = self.layout.grid_flow(columns=3, even_columns=False, even_rows=False, align=False) + + col1 = grid.column(align=True) + col2 = grid.column(align=False) + col3 = grid.column(align=True) + col1.ui_units_x = 1 + col2.ui_units_x = 1 + col3.ui_units_x = 2 + + row1_col1 = col1.row() + row1_col1.label(text="Schedule Work") + row1col2 = col2.row() + schedule_work = resource.get("ScheduleWork", None) + derived_schedule_work = resource.get("DerivedScheduleWork", None) + row1col2.label( + text="{}".format(schedule_work) if schedule_work else "{} h*".format(derived_schedule_work), + icon="TIME", + ) + + row1col3 = col3.row() + row1col3.operator("bim.calculate_resource_work", text="", icon="TEMP").resource = ifc_definition_id + op = row1col3.operator( + "bim.add_usage_constraint" if not is_work_locked else "bim.remove_usage_constraint", + text="", + icon="LOCKED" if is_work_locked else "UNLOCKED", + ) + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleWork" + row2_col1 = col1.row() + row2_col1.label(text="Schedule Usage") + row2col2 = col2.row() + row2col2.prop(self.tprops.resources[self.props.active_resource_index], "schedule_usage", text="") + row2col3 = col3.row() + row2col3.operator("bim.calculate_resource_usage", text="", icon="TEMP") + op = row2col3.operator( + "bim.add_usage_constraint" if not is_usage_locked else "bim.remove_usage_constraint", + text="", + icon="LOCKED" if is_usage_locked else "UNLOCKED", + ) + op.resource = ifc_definition_id + op.attribute = "Usage.ScheduleUsage" + + productivity = resource["Productivity"] + parent_productivity = resource["InheritedProductivity"] + row = self.layout.row() + if productivity: + produtivitiy_rate_message = "Current Productivity Rate: {} {} / {}".format( + productivity["QuantityProduced"], + productivity["QuantityProducedName"], + productivity["TimeConsumed"], + ) + row.alignment = "LEFT" + row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA") + row.operator("bim.edit_productivity_data", text="", icon="GREASEPENCIL") + op = row.operator("bim.remove_pset", text="", icon="X") + op.pset_id = productivity["id"] + op.obj_type = "Resource" + op.obj = "" + + elif parent_productivity: + produtivitiy_rate_message = "Inherited Productivity Rate: {} {} / {}*".format( + parent_productivity["QuantityProduced"], + parent_productivity["QuantityProducedName"], + parent_productivity["TimeConsumed"], + ) + row.alignment = "LEFT" + row.label(text="{}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") + row.operator("bim.add_productivity_data", text="", icon="ADD") + else: + row = self.layout.row(align=True) + row.alignment = "LEFT" + produtivitiy_rate_message = "No productivity data found" + row.label(text="{}".format(produtivitiy_rate_message), icon="ARMATURE_DATA") + row.operator("bim.add_productivity_data", text="", icon="ADD") def draw_resource_operators(self): row = self.layout.row(align=True) @@ -153,9 +220,6 @@ class BIM_PT_resources(Panel): if not self.props.active_resource_id: if resource["type"] in ["IfcLaborResource", "IfcConstructionEquipmentResource"]: - if resource["Productivity"]: - op = row.operator("bim.calculate_resource_work", text="", icon="TEMP") - op.resource = ifc_definition_id row.operator("bim.enable_editing_resource_time", text="", icon="TIME").resource = ifc_definition_id op = row.operator("bim.enable_editing_resource_base_quantity", text="", icon="PROPERTIES") op.resource = ifc_definition_id @@ -163,6 +227,12 @@ class BIM_PT_resources(Panel): op.resource = ifc_definition_id row.operator("bim.enable_editing_resource", text="", icon="GREASEPENCIL").resource = ifc_definition_id row.operator("bim.remove_resource", text="", icon="X").resource = ifc_definition_id + else: + if self.props.editing_resource_type == "ATTRIBUTES": + row.operator("bim.edit_resource", text="", icon="CHECKMARK") + elif self.props.editing_resource_type == "USAGE": + row.operator("bim.edit_resource_time", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_resource", text="", icon="CANCEL") def draw_editable_resource_attributes_ui(self): blenderbim.bim.helper.draw_attributes(self.props.resource_attributes, self.layout) @@ -250,7 +320,6 @@ class BIM_PT_resources(Panel): def draw_duration_property(self, duration_props, layout): for duration_prop in duration_props: if duration_prop.name == "BaseQuantityConsumed": - layout.label(text=duration_prop.name) layout.prop(duration_prop, "years", text="Y") layout.prop(duration_prop, "months", text="M") layout.prop(duration_prop, "days", text="D") @@ -287,7 +356,7 @@ class BIM_UL_resources(UIList): else: row.label(text="", icon="DOT") row.prop(item, "name", emboss=False, text="", icon=icon_map[resource["type"]]) - row.prop(item, "schedule_usage", text="", emboss=False) + row.prop(item, "schedule_usage", text="", emboss=False) if item.schedule_usage else None if context.active_object and not props.active_resource_id: row = layout.row(align=True) if item.ifc_definition_id in ResourceData.data["active_resource_ids"]: @@ -303,3 +372,29 @@ class BIM_UL_resources(UIList): elif props.editing_resource_type == "USAGE": row.operator("bim.edit_resource_time", text="", icon="CHECKMARK") row.operator("bim.disable_editing_resource", text="", icon="CANCEL") + + +def draw_productivity_ui(self, context): + def draw_duration_property(duration_props, layout): + for duration_prop in duration_props: + if duration_prop.name == "BaseQuantityConsumed": + layout.prop(duration_prop, "years", text="Y") + layout.prop(duration_prop, "months", text="M") + layout.prop(duration_prop, "days", text="D") + layout.prop(duration_prop, "hours", text="H") + layout.prop(duration_prop, "minutes", text="Min") + layout.prop(duration_prop, "seconds", text="S") + + productivity_props = context.scene.BIMResourceProductivity + grid = self.layout.grid_flow(columns=2, even_columns=False, even_rows=False, align=False) + col1 = grid.column(align=False) + col2 = grid.column(align=False) + row1_col1 = col1.row() + row1_col1.label(text="Quantity") + row1_col2 = col2.row() + row1_col2.prop(productivity_props, "quantity_produced", text="") + row1_col2.prop(productivity_props, "quantity_produced_name", text="") + row2_col1 = col1.row() + row2_col1.label(text="Time") + row2_col2 = col2.row() + draw_duration_property(productivity_props.quantity_consumed, row2_col2) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 23779b4616..bb89b8152f 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -82,7 +82,7 @@ classes = ( operator.ExportP6, operator.GenerateGanttChart, operator.GuessDateRange, - operator.HighlightTask, + operator.GoToTask, operator.ImportCSV, operator.ImportMSP, operator.ImportP6, @@ -118,12 +118,18 @@ classes = ( operator.UnassignWorkSchedule, operator.VisualiseWorkScheduleDate, operator.VisualiseWorkScheduleDateRange, + operator.EnableStatusFilters, + operator.DisableStatusFilters, + operator.ActivateStatusFilters, + operator.SelectStatusFilter, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, prop.TaskResource, prop.TaskProduct, prop.ISODuration, + prop.IFCStatus, + prop.BIMStatusProperties, prop.BIMWorkScheduleProperties, prop.BIMTaskTreeProperties, prop.BIMTaskTypeColor, @@ -133,6 +139,7 @@ classes = ( prop.BIMWorkCalendarProperties, prop.DatePickerProperties, prop.BIMDateTextProperties, + ui.BIM_PT_status, ui.BIM_PT_work_plans, ui.BIM_PT_work_schedules, ui.BIM_PT_work_calendars, @@ -165,6 +172,7 @@ def menu_func_import(self, context): def register(): + bpy.types.Scene.BIMStatusProperties = bpy.props.PointerProperty(type=prop.BIMStatusProperties) bpy.types.Scene.BIMWorkPlanProperties = bpy.props.PointerProperty(type=prop.BIMWorkPlanProperties) bpy.types.Scene.BIMWorkScheduleProperties = bpy.props.PointerProperty(type=prop.BIMWorkScheduleProperties) bpy.types.Scene.BIMTaskTreeProperties = bpy.props.PointerProperty(type=prop.BIMTaskTreeProperties) @@ -177,6 +185,7 @@ def register(): def unregister(): + del bpy.types.Scene.BIMStatusProperties del bpy.types.Scene.BIMWorkPlanProperties del bpy.types.Scene.BIMWorkScheduleProperties del bpy.types.Scene.BIMTaskTreeProperties diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 08b48c979a..ddcfc309e4 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -27,11 +27,98 @@ import webbrowser import blenderbim.core.sequence as core import blenderbim.tool as tool import blenderbim.bim.module.sequence.helper as helper +import ifcopenshell.util.sequence +import ifcopenshell.util.selector from datetime import datetime from dateutil import parser, relativedelta from blenderbim.bim.ifc import IfcStore from bpy_extras.io_utils import ImportHelper -import ifcopenshell.util.sequence + + +class EnableStatusFilters(bpy.types.Operator): + bl_idname = "bim.enable_status_filters" + bl_label = "Enable Status Filters" + + def execute(self, context): + props = context.scene.BIMStatusProperties + props.is_enabled = True + + props.statuses.clear() + + statuses = set() + for element in tool.Ifc.get().by_type("IfcPropertyEnumeratedValue"): + if element.Name == "Status": + pset = element.PartOfPset[0] + if pset.Name.startswith("Pset_") and pset.Name.endswith("Common"): + statuses.update(element.EnumerationValues) + elif pset.Name == "EPset_Status": # Our secret sauce + statuses.update(element.EnumerationValues) + elif element.Name == "UserDefinedStatus": + statuses.add(element.NominalValue) + + statuses = ["No Status"] + sorted([s.wrappedValue for s in statuses]) + + for status in statuses: + new = props.statuses.add() + new.name = status + return {"FINISHED"} + + +class DisableStatusFilters(bpy.types.Operator): + bl_idname = "bim.disable_status_filters" + bl_label = "Disable Status Filters" + + def execute(self, context): + props = context.scene.BIMStatusProperties + props.is_enabled = False + return {"FINISHED"} + + +class ActivateStatusFilters(bpy.types.Operator): + bl_idname = "bim.activate_status_filters" + bl_label = "Activate Status Filters" + + def execute(self, context): + props = context.scene.BIMStatusProperties + + query = [] + visible_statuses = {s.name for s in props.statuses if s.is_visible} + for name in visible_statuses: + if name == "No Status": + q = f"IfcProduct, /Pset_.*Common/.Status=NULL, EPset_Status.Status=NULL" + else: + q = f"IfcProduct, /Pset_.*Common/.Status={name} + IfcProduct, EPset_Status.Status={name}" + query.append(q) + query = " + ".join(query) + + if not query: + return {"FINISHED"} + + visible_elements = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + + for obj in bpy.context.view_layer.objects: + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcProduct"): + continue + obj.hide_set(element not in visible_elements) + return {"FINISHED"} + + +class SelectStatusFilter(bpy.types.Operator): + bl_idname = "bim.select_status_filter" + bl_label = "Select Status Filter" + name: bpy.props.StringProperty() + + def execute(self, context): + props = context.scene.BIMStatusProperties + query = f"IfcProduct, /Pset_.*Common/.Status={self.name} + IfcProduct, EPset_Status.Status={self.name}" + if self.name == "No Status": + query = f"IfcProduct, /Pset_.*Common/.Status=NULL, EPset_Status.Status=NULL" + for element in ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query): + obj = tool.Ifc.get_object(element) + if obj: + obj.select_set(True) + return {"FINISHED"} class AddWorkPlan(bpy.types.Operator, tool.Ifc.Operator): @@ -1323,14 +1410,14 @@ class LoadProductTasks(bpy.types.Operator): return {"FINISHED"} -class HighlightTask(bpy.types.Operator): - bl_idname = "bim.highlight_task" +class GoToTask(bpy.types.Operator): + bl_idname = "bim.go_to_task" bl_label = "Highlight Task" bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): - r = core.highlight_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task)) + r = core.go_to_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task)) if isinstance(r, str): self.report({"WARNING"}, r) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 711e783f9f..4ea2f667d8 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -24,7 +24,8 @@ 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 -from blenderbim.bim.module.sequence.data import SequenceData, AnimationColorSchemeData +from blenderbim.bim.module.sequence.data import SequenceData, AnimationColorSchemeData, refresh as refresh_sequence_data +import blenderbim.bim.module.resource.data import blenderbim.bim.module.pset.data from blenderbim.bim.prop import StrProperty, Attribute from dateutil import parser @@ -224,7 +225,9 @@ def updateTaskDuration(self, context): 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() + blenderbim.core.sequence.load_task_properties(tool.Sequence) bpy.ops.bim.load_task_properties() + tool.Sequence.load_resources() def get_schedule_predefined_types(self, context): @@ -325,6 +328,25 @@ def get_saved_color_schemes(self, context): return AnimationColorSchemeData.data["saved_color_schemes"] +def updateAssignedResourceName(self, context): + pass + +def updateAssignedResourceUsage(self, context): + if not self.schedule_usage: + return + resource = tool.Ifc.get().by_id(self.ifc_definition_id) + if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage: + return + tool.Resource.run_edit_resource_time(resource, attributes={ + "ScheduleUsage": self.schedule_usage + }) + tool.Sequence.load_task_properties() + tool.Resource.load_resource_properties() + tool.Sequence.refresh_task_resources() + blenderbim.bim.module.resource.data.refresh() + blenderbim.bim.module.sequence.data.refresh() + blenderbim.bim.module.pset.data.refresh() + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) identification: StringProperty(name="Identification", update=updateTaskIdentification) @@ -352,9 +374,9 @@ class WorkPlan(PropertyGroup): class TaskResource(PropertyGroup): - name: StringProperty(name="Name") + name: StringProperty(name="Name", update=updateAssignedResourceName) ifc_definition_id: IntProperty(name="IFC Definition ID") - schedule_usage: FloatProperty(name="Schedule Usage") + schedule_usage: FloatProperty(name="Schedule Usage", update=updateAssignedResourceUsage) class TaskProduct(PropertyGroup): @@ -381,6 +403,16 @@ class ISODuration(PropertyGroup): seconds: IntProperty(name="Seconds", default=0) +class IFCStatus(PropertyGroup): + name: StringProperty(name="Name") + is_visible: BoolProperty(name="Is Visible", default=True) + + +class BIMStatusProperties(PropertyGroup): + is_enabled: BoolProperty(name="Is Enabled") + statuses: CollectionProperty(name="Statuses", type=IFCStatus) + + class BIMWorkScheduleProperties(PropertyGroup): work_schedule_predefined_types: EnumProperty( items=get_schedule_predefined_types, name="Predefined Type", default=None diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 5339fcae58..f28794e481 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -30,6 +30,38 @@ from blenderbim.bim.module.sequence.data import ( ) +class BIM_PT_status(Panel): + bl_label = "Status" + bl_idname = "BIM_PT_status" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_tab_status" + bl_options = {"HIDE_HEADER"} + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + self.props = context.scene.BIMStatusProperties + + if not self.props.is_enabled: + row = self.layout.row() + row.operator("bim.enable_status_filters", icon="GREASEPENCIL") + return + + row = self.layout.row(align=True) + row.operator("bim.activate_status_filters", icon="TIME") + row.operator("bim.disable_status_filters", icon="CANCEL", text="") + + for status in self.props.statuses: + row = self.layout.row() + row.label(text=status.name) + row.prop(status, "is_visible", text="", emboss=False, icon="HIDE_OFF" if status.is_visible else "HIDE_ON") + row.operator("bim.select_status_filter", icon="RESTRICT_SELECT_OFF", text="").name = status.name + + class BIM_PT_work_plans(Panel): bl_label = "Work Plans" bl_idname = "BIM_PT_work_plans" @@ -37,7 +69,7 @@ class BIM_PT_work_plans(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): @@ -107,7 +139,7 @@ class BIM_PT_work_schedules(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): @@ -319,6 +351,7 @@ class BIM_PT_work_schedules(Panel): def draw_editable_sequence_ui(self, sequence, process_type): task = SequenceData.data["tasks"][sequence[process_type]] row = self.layout.row(align=True) + row.operator("bim.go_to_task", text="", icon="RESTRICT_SELECT_OFF").task = task["id"] row.label(text=task["Identification"] or "XXX") row.label(text=task["Name"] or "Unnamed") row.label(text=sequence["SequenceType"] or "N/A") @@ -550,7 +583,7 @@ class BIM_PT_animation_Color_Scheme(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): @@ -733,9 +766,9 @@ class BIM_UL_task_resources(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) + row.operator("bim.go_to_resource", text="", icon="STYLUS_PRESSURE").resource = item.ifc_definition_id row.prop(item, "name", emboss=False, text="") - row.label(text=str(item.schedule_usage)) - + row.prop(item, "schedule_usage", emboss=False, text="") class BIM_UL_animation_colors(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): @@ -758,7 +791,7 @@ class BIM_UL_product_input_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) - op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE") + op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE") op.task = item.ifc_definition_id row.split(factor=0.8) row.label(text=item.name) @@ -768,7 +801,7 @@ class BIM_UL_product_output_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) - op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE") + op = row.operator("bim.go_to_task", text="", icon="STYLUS_PRESSURE") op.task = item.ifc_definition_id row.split(factor=0.8) row.label(text=item.name) @@ -888,7 +921,7 @@ class BIM_PT_work_calendars(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_tab_4D5D" + bl_parent_id = "BIM_PT_tab_sequence" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index 2347e63c4c..de0b978650 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -115,8 +115,17 @@ class BIM_PT_booleans(Panel): row.operator("bim.add_boolean", text="Apply Boolean", icon="ADD") show_boolean_button = row.row(align=True) show_boolean_button.operator("bim.show_booleans", text="", icon="HIDE_OFF") - show_boolean_button.enabled = BooleansData.data['total_booleans'] > 0 + show_boolean_button.enabled = BooleansData.data["total_booleans"] > 0 row.operator("bim.hide_booleans", text="", icon="HIDE_ON") + elif context.active_object.data.BIMMeshProperties.ifc_boolean_id: + upsteam_obj = context.active_object.data.BIMMeshProperties.obj + upstream_obj_ifc_id = upsteam_obj.BIMObjectProperties.ifc_definition_id + + row = layout.row(align=True) + row.label(text="Used as a boolean operand with:") + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = upstream_obj_ifc_id + row.label(text=upsteam_obj.name) + row = layout.row() row.operator("bim.remove_booleans", text="Remove Boolean", icon="X") diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 03c8514ee1..706a3b8163 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -298,7 +298,7 @@ class BIM_PT_tabs(Panel): self.draw_tab_entry(row, "NETWORK_DRIVE", "SERVICES", is_ifc_project, aprops.tab == "SERVICES") self.draw_tab_entry(row, "EDITMODE_HLT", "STRUCTURE", is_ifc_project, aprops.tab == "STRUCTURE") self.draw_tab_entry(row, "NLA", "SCHEDULING", is_ifc_project, aprops.tab == "SCHEDULING") - self.draw_tab_entry(row, "PACKAGE", "FM", is_ifc_project, aprops.tab == "FM") + self.draw_tab_entry(row, "PACKAGE", "FM", True, aprops.tab == "FM") self.draw_tab_entry(row, "COMMUNITY", "QUALITY", True, aprops.tab == "QUALITY") self.draw_tab_entry(row, "BLENDER", "BLENDER", True, aprops.tab == "BLENDER") row.operator("bim.switch_tab", text="", emboss=False, icon="UV_SYNC_SELECT") @@ -408,12 +408,53 @@ class BIM_PT_geometry(Panel): pass -class BIM_PT_tab_4D5D(Panel): - bl_label = "Costing and Scheduling" +class BIM_PT_tab_status(Panel): + bl_label = "Status" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @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_resources(Panel): + bl_label = "Resources" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @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_cost(Panel): + bl_label = "Cost" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @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_sequence(Panel): + bl_label = "Construction Scheduling" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): @@ -464,6 +505,7 @@ class BIM_PT_tab_quality_control(Panel): def draw(self, context): pass + class BIM_PT_tab_clash_detection(Panel): bl_label = "Clash Detection" bl_space_type = "PROPERTIES" @@ -477,6 +519,7 @@ class BIM_PT_tab_clash_detection(Panel): def draw(self, context): pass + class BIM_PT_tab_sandbox(Panel): bl_label = "Sandbox" bl_space_type = "PROPERTIES" @@ -626,7 +669,7 @@ class BIM_PT_tab_handover(Panel): @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "FM") and tool.Ifc.get() + return tool.Blender.is_tab(context, "FM") def draw(self, context): pass @@ -641,7 +684,7 @@ class BIM_PT_tab_operations(Panel): @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "FM") and tool.Ifc.get() + return tool.Blender.is_tab(context, "FM") def draw(self, context): pass @@ -658,24 +701,13 @@ class UIData: @classmethod def version(cls): - return ".".join( - [ - str(x) - for x in [ - addon.bl_info.get("version", (-1, -1, -1)) - for addon in addon_utils.modules() - if addon.bl_info["name"] == "BlenderBIM" - ][0] - ] - ) + return tool.Blender.get_blenderbim_version() def draw_statusbar(self, context): if not UIData.is_loaded: UIData.load() text = f"BlenderBIM Add-on v{UIData.data['version']}" - if blenderbim.bim.last_commit_hash != "8888888": - text += f"-{blenderbim.bim.last_commit_hash[:7]}" self.layout.label(text=text) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 167b8ae4d8..c16b341b1c 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -25,6 +25,14 @@ def load_brick_project(brick, filepath=None, brick_root=None): brick.set_active_brick_class(brick_root, split_screen=True) +def new_brick_file(brick, brick_root=None): + brick.new_brick_file() + brick.import_brick_classes(brick_root) + brick.import_brick_classes(brick_root, split_screen=True) + brick.set_active_brick_class(brick_root) + brick.set_active_brick_class(brick_root, split_screen=True) + + def view_brick_class(brick, brick_class=None, split_screen=False): brick.add_brick_breadcrumb(split_screen=split_screen) brick.clear_brick_browser(split_screen=split_screen) @@ -35,7 +43,7 @@ def view_brick_class(brick, brick_class=None, split_screen=False): def view_brick_item(brick, item=None, split_screen=False): brick_class = brick.get_item_class(item) - view_brick_class(brick, brick_class=brick_class, split_screen=split_screen) + brick.run_view_brick_class(brick_class=brick_class, split_screen=split_screen) brick.select_browser_item(item, split_screen=split_screen) @@ -79,18 +87,17 @@ def add_brick(ifc, brick, element=None, namespace=None, brick_class=None, librar if library: brick.run_assign_brick_reference(element=element, library=library, brick_uri=brick_uri) else: - brick_uri = brick.add_brick(namespace, brick_class, label) + brick.add_brick(namespace, brick_class, label) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) def add_brick_relation(brick, brick_uri=None, predicate=None, object=None): brick.add_relation(brick_uri, predicate, object) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) def convert_ifc_to_brick(brick, namespace=None, library=None): + # convert spaces to brick spaces = brick.get_convertable_brick_spaces() space_uris = {} for space in spaces: @@ -98,57 +105,43 @@ def convert_ifc_to_brick(brick, namespace=None, library=None): space_uris[space] = brick_uri if library: brick.run_assign_brick_reference(element=space, library=library, brick_uri=brick_uri) - for space in spaces: parent = brick.get_parent_space(space) if parent: - brick.add_relation(space_uris[parent], "hasPart", space_uris[space]) - + brick.add_relation(space_uris[parent], "https://brickschema.org/schema/Brick#hasPart", space_uris[space]) + # convert systems to brick systems = brick.get_convertable_brick_systems() system_uris = {} for system in systems: brick_uri = brick.add_brick_from_element(system, namespace, brick.get_brick_class(system)) - space_uris[space] = brick_uri + system_uris[system] = brick_uri if library: brick.run_assign_brick_reference(element=system, library=library, brick_uri=brick_uri) - + # convert services to brick distribution_elements = brick.get_convertable_brick_elements() equipment_uris = {} for element in distribution_elements: brick_uri = brick.add_brick_from_element(element, namespace, brick.get_brick_class(element)) equipment_uris[element] = brick_uri space = brick.get_element_container(element) - brick.add_relation(brick_uri, "hasLocation", space_uris[space]) + brick.add_relation(brick_uri, "https://brickschema.org/schema/Brick#hasLocation", space_uris[space]) systems = brick.get_element_systems(element) for system in systems: - brick.add_relation(system_uris[system], "hasPart", brick_uri) + brick.add_relation(system_uris[system], "https://brickschema.org/schema/Brick#hasPart", brick_uri) if library: brick.run_assign_brick_reference(element=element, library=library, brick_uri=brick_uri) - - distribution_elements = brick.get_convertable_brick_elements() for element in distribution_elements: feeds = brick.get_element_feeds(element) for downstream_equipment in feeds: - brick.add_relation(equipment_uris[element], "feeds", equipment_uris[downstream_equipment]) - + brick.add_relation(equipment_uris[element], "https://brickschema.org/schema/Brick#feeds", equipment_uris[downstream_equipment]) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) -def new_brick_file(brick, brick_root=None): - brick.new_brick_file() - brick.import_brick_classes(brick_root) - brick.import_brick_classes(brick_root, split_screen=True) - brick.set_active_brick_class(brick_root) - brick.set_active_brick_class(brick_root, split_screen=True) - - -def refresh_brick_viewer(brick, split_screen=False): - if split_screen: - brick.run_view_brick_class(brick_class=brick.get_active_brick_class(split_screen=split_screen), split_screen=split_screen) - else: - brick.run_view_brick_class(brick_class=brick.get_active_brick_class(), split_screen=split_screen) - brick.pop_brick_breadcrumb(split_screen=split_screen) +def refresh_brick_viewer(brick): + brick.run_view_brick_class(brick_class=brick.get_active_brick_class()) + brick.pop_brick_breadcrumb() + brick.run_view_brick_class(brick_class=brick.get_active_brick_class(split_screen=True), split_screen=True) + brick.pop_brick_breadcrumb(split_screen=True) def remove_brick(ifc, brick, library=None, brick_uri=None): @@ -158,14 +151,13 @@ def remove_brick(ifc, brick, library=None, brick_uri=None): ifc.run("library.remove_reference", reference=reference) brick.remove_brick(brick_uri) brick.run_refresh_brick_viewer() - brick.run_refresh_brick_viewer(split_screen=True) def serialize_brick(brick): brick.serialize_brick() -def add_namespace(brick, alias=None, uri=None): +def add_brick_namespace(brick, alias=None, uri=None): brick.add_namespace(alias, uri) @@ -176,4 +168,3 @@ def set_brick_list_root(brick, brick_root=None, split_screen=False): def remove_brick_relation(brick, brick_uri=None, predicate=None, object=None): brick.remove_relation(brick_uri, predicate, object) - brick.run_refresh_brick_viewer() diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index 0beafa9b9b..c026139446 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -23,11 +23,9 @@ def load_resources(resource): resource.load_resources() resource.load_resource_properties() - def add_resource(tool_ifc, resource_tool, ifc_class, parent_resource=None): tool_ifc.run("resource.add_resource", ifc_class=ifc_class, parent_resource=parent_resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def load_resource_properties(resource_tool, resource=None): @@ -56,8 +54,7 @@ def edit_resource(ifc, resource_tool, resource): def remove_resource(ifc, resource_tool, resource=None): ifc.run("resource.remove_resource", resource=resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def enable_editing_resource_time(ifc_tool, resource_tool, resource): @@ -79,9 +76,13 @@ def disable_editing_resource_time(resource_tool): def calculate_resource_work(ifc, resource_tool, resource): - ifc.run("resource.calculate_resource_work", resource=resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + if resource_tool.get_task_assignments(resource): + ifc.run("resource.calculate_resource_work", resource=resource) + else: + nested_resources = resource_tool.get_nested_resources(resource) + for nested_resource in nested_resources or []: + ifc.run("resource.calculate_resource_work", resource=nested_resource) + load_resources(resource_tool) def enable_editing_resource_costs(resource_tool, resource): @@ -142,20 +143,17 @@ def edit_resource_quantity(resource_tool, ifc, physical_quantity=None): def import_resources(resource_tool, file_path): resource_tool.import_resources(file_path) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def expand_resource(resource_tool, resource): resource_tool.expand_resource(resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def contract_resource(resource_tool, resource): resource_tool.contract_resource(resource) - resource_tool.load_resources() - resource_tool.load_resource_properties() + load_resources(resource_tool) def assign_resource(ifc, spatial, resource=None, products=None): @@ -182,3 +180,42 @@ def edit_productivity_pset(ifc, resource_tool): else: pset = ifc.run("pset.add_pset", product=resource, name="EPset_Productivity") ifc.run("pset.edit_pset", pset=pset, properties=resource_tool.get_productivity_attributes()) + + +def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None): + metric = resource_tool.has_metric_constraint(resource, "Usage") + if metric: + return print("Must remove existing metric first") + + objective = ifc.run("constraint.add_objective") + ifc.run("constraint.edit_objective", objective=objective, attributes={"ObjectiveQualifier": "PARAMETER"}) + metric = ifc.run("constraint.add_metric", objective=objective) + ifc.run( + "constraint.edit_metric", + metric=metric, + attributes={ + "ConstraintGrade": "HARD", + "Benchmark": "EQUALTO", + }, + ) + ifc.run("constraint.add_metric_reference", metric=metric, reference_path=reference_path) + ifc.run("constraint.assign_constraint", product=resource, constraint=objective) + + +def remove_usage_constraint(ifc, resource_tool, resource, reference_path): + constraints = resource_tool.get_constraints(resource) + for constraint in constraints: + metrics = resource_tool.get_metrics(constraint) + for metric in metrics: + reference = resource_tool.get_metric_reference(metric, is_deep=True) + if reference == reference_path: + ifc.run("constraint.remove_metric", metric=metric) + ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint) + ifc.run("constraint.remove_constraint", constraint=constraint) + +def go_to_resource(resource_tool, resource): + resource_tool.go_to_resource(resource) + +def calculate_resource_usage(ifc, resource_tool, resource): + ifc.run("resource.calculate_resource_usage", resource=resource) + load_resources(resource_tool) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index eb3bdbc8c9..2321796783 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -193,6 +193,7 @@ def edit_task_time(ifc, sequence, task_time=None): task = sequence.get_active_task() sequence.load_task_properties(task=task) sequence.disable_editing_task_time() + sequence.load_resources() def assign_predecessor(ifc, sequence, task=None): @@ -465,11 +466,11 @@ def load_animation_color_scheme(sequence, scheme): sequence.load_animation_color_scheme(scheme) -def highlight_task(sequence, task=None): +def go_to_task(sequence, task=None): work_schedule = sequence.get_work_schedule(task) is_work_schedule_active = sequence.is_work_schedule_active(work_schedule) if is_work_schedule_active: - sequence.highlight_task(task) + sequence.go_to_task(task) else: return "Work schedule is not active" @@ -485,7 +486,7 @@ def highlight_product_related_task(sequence, spatial, product_type=None): work_schedule = sequence.get_work_schedule(task) is_work_schedule_active = sequence.is_work_schedule_active(work_schedule) if is_work_schedule_active: - sequence.highlight_task(task) + sequence.go_to_task(task) def guess_date_range(sequence, work_schedule=None): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 797f556e25..f0f168a7ba 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -94,36 +94,46 @@ class Boundary: pass @interface class Brick: - def add_brick(cls, namespace, brick_class): pass - def add_brick_breadcrumb(cls): pass + def add_brick(cls, namespace, brick_class, label): pass + def add_brick_breadcrumb(cls, split_screen=False): pass def add_brick_from_element(cls, element, namespace, brick_class): pass def add_brickifc_project(cls, namespace): pass def add_brickifc_reference(cls, brick, element, project): pass - def add_feed(cls, source, destination): pass - def clear_brick_browser(cls): pass + def add_relation(cls, brick_uri, predicate, object): pass + def remove_relation(cls, brick_uri, predicate, object): pass + def clear_brick_browser(cls, split_screen=False): pass def clear_project(cls): pass def export_brick_attributes(cls, brick_uri): pass - def get_active_brick_class(cls): pass + def get_active_brick_class(cls, split_screen=False): pass def get_brick(cls, element): pass def get_brick_class(cls, element): pass def get_brick_path(cls): pass def get_brick_path_name(cls): pass def get_brickifc_project(cls): pass def get_convertable_brick_elements(cls): pass + def get_convertable_brick_spaces(cls): pass + def get_convertable_brick_systems(cls): pass + def get_parent_space(cls, space): pass + def get_element_container(cls, element): pass + def get_element_systems(cls, element): pass + def get_element_feeds(cls, element): pass def get_item_class(cls, item): pass def get_library_brick_reference(cls, library, brick_uri): pass def get_namespace(cls, uri): pass - def import_brick_classes(cls, brick_class): pass - def import_brick_items(cls, brick_class): pass + def import_brick_classes(cls, brick_class, split_screen=False): pass + def import_brick_items(cls, brick_class, split_screen=False): pass def load_brick_file(cls, filepath): pass def new_brick_file(cls): pass - def pop_brick_breadcrumb(cls): pass + def pop_brick_breadcrumb(cls, split_screen=False): pass def remove_brick(cls, brick_uri): pass def run_assign_brick_reference(cls, element=None, library=None, brick_uri=None): pass - def run_refresh_brick_viewer(cls): pass - def run_view_brick_class(cls, brick_class=None): pass - def select_browser_item(cls, item): pass - def set_active_brick_class(cls, brick_class): pass + def run_refresh_brick_viewer(cls, split_screen=False): pass + def run_view_brick_class(cls, brick_class=None, split_screen=False): pass + def select_browser_item(cls, item, split_screen=False): pass + def set_active_brick_class(cls, brick_class, split_screen=False): pass + def serialize_brick(cls): pass + def add_namespace(cls, alias, uri): pass + def clear_breadcrumbs(cls, split_screen=False): pass @interface @@ -241,6 +251,7 @@ class Drawing: def activate_drawing(cls, camera): pass def add_literal_to_annotation(cls, obj, Literal='Literal', Path='RIGHT', BoxAlignment='bottom-left'): pass def copy_representation(cls, source, dest): pass + def create_annotation_context(cls, target_view, object_type=None): pass def create_annotation_object(cls, drawing, object_type): pass def create_camera(cls, name, matrix): pass def create_svg_schedule(cls, schedule): pass @@ -601,7 +612,10 @@ class Resource: def enable_editing_resource_time(cls, resource): pass def enable_editing_resource(cls, resource): pass def expand_resource(cls, resource): pass + def get_constraints(cls, resource): pass def get_highlighted_resource(cls): pass + def get_metric_reference(cls, metric, is_deep): pass + def get_metrics(cls, constraint): pass def get_productivity_attributes(cls): pass def get_productivity(cls, resource, should_inherit): pass def get_resource_attributes(cls): pass @@ -610,6 +624,8 @@ class Resource: def get_resource_quantity_attributes(cls): pass def get_resource_time_attributes(cls): pass def get_resource_time(cls, resource): pass + def go_to_resource(cls, resource): pass + def has_metric_constraint(cls, resource): pass def import_resources(cls, file_path): pass def load_cost_value_attributes(cls, cost_value): pass def load_productivity_data(cls): pass @@ -618,6 +634,7 @@ class Resource: def load_resource_time_attributes(cls, resource_time): pass def load_resources(cls): pass + @interface class Root: def add_tracked_opening(cls, obj): pass @@ -728,7 +745,7 @@ class Sequence: def get_work_time_attributes(cls): pass def guess_date_range(cls, work_schedule): pass def has_task_assignments(cls, product, cost_schedule=None): pass - def highlight_task(cls, task): pass + def go_to_task(cls, task): pass def is_filter_by_active_schedule(cls): pass def is_work_schedule_active(cls, work_schedule): pass def load_animation_color_scheme(cls, scheme): pass @@ -759,6 +776,7 @@ class Sequence: def show_snapshot(cls, product_states): pass def update_task_ICOM(cls, task): pass def update_visualisation_date(cls, start_date, finish_date): pass + def refresh_task_resources(cls): pass @interface diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index f3702bb801..0ce77b6d1a 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -20,8 +20,10 @@ import bpy import json import ifcopenshell.api import blenderbim.tool as tool +import blenderbim.bim from mathutils import Vector from pathlib import Path +import addon_utils VIEWPORT_ATTRIBUTES = [ @@ -588,3 +590,19 @@ class Blender: child_obj = tool.Blender.get_object_from_guid(child_guid) if child_obj: yield child_obj + + @classmethod + def get_blenderbim_version(cls): + version = ".".join( + [ + str(x) + for x in [ + addon.bl_info.get("version", (-1, -1, -1)) + for addon in addon_utils.modules() + if addon.bl_info["name"] == "BlenderBIM" + ][0] + ] + ) + if blenderbim.bim.last_commit_hash != "8888888": + version += f"-{blenderbim.bim.last_commit_hash[:7]}" + return version diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 45b79b04e6..847adfe09f 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -105,16 +105,16 @@ class Brick(blenderbim.core.tool.Brick): if predicate == "http://www.w3.org/2000/01/rdf-schema#label": with BrickStore.new_changeset() as cs: cs.add((URIRef(brick_uri), URIRef(predicate), Literal(object))) - bpy.context.scene.BIMBrickProperties.new_brick_relation_type = BrickStore.relationships[0][0] - bpy.context.scene.BIMBrickProperties.add_relation_failed = False + bpy.context.scene.BIMBrickProperties.new_brick_relation_type = BrickStore.relationships[0] + bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = False return query = BrickStore.graph.query("ASK { <{object_uri}> a ?o . }".replace("{object_uri}", object)) if query: with BrickStore.new_changeset() as cs: cs.add((URIRef(brick_uri), URIRef(predicate), URIRef(object))) - bpy.context.scene.BIMBrickProperties.add_relation_failed = False + bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = False else: - bpy.context.scene.BIMBrickProperties.add_relation_failed = True + bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = True @classmethod def remove_relation(cls, brick_uri, predicate, object): @@ -141,10 +141,23 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def export_brick_attributes(cls, brick_uri): + query = BrickStore.graph.query( + """ + PREFIX rdfs: + SELECT ?label { + <{brick_uri}> rdfs:label ?label . + } + LIMIT 1 + """.replace("{brick_uri}", brick_uri)) + name = None + for row in query: + name = str(row.get("label")) + if not name: + name = brick_uri.split("#")[-1] if tool.Ifc.get_schema() == "IFC2X3": - return {"ItemReference": brick_uri, "Name": brick_uri.split("#")[-1]} + return {"ItemReference": brick_uri, "Name": name} else: - return {"Identification": brick_uri, "Name": brick_uri.split("#")[-1]} + return {"Identification": brick_uri, "Name": name} @classmethod def get_active_brick_class(cls, split_screen=False): @@ -204,8 +217,8 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def get_convertable_brick_spaces(cls): if tool.Ifc.get_schema() == "IFC2X3": - return tool.Ifc.get().by_type("IfcSpatialStructureElement") - return tool.Ifc.get().by_type("IfcSpatialElement") + return set(tool.Ifc.get().by_type("IfcSpatialStructureElement")) + return set(tool.Ifc.get().by_type("IfcSpatialElement")) @classmethod def get_convertable_brick_systems(cls): @@ -337,7 +350,7 @@ 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.set_last_saved() BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() @@ -386,8 +399,8 @@ class Brick(blenderbim.core.tool.Brick): ) @classmethod - def run_refresh_brick_viewer(cls, split_screen=False): - return blenderbim.core.brick.refresh_brick_viewer(tool.Brick, split_screen) + def run_refresh_brick_viewer(cls): + return blenderbim.core.brick.refresh_brick_viewer(tool.Brick) @classmethod def run_view_brick_class(cls, brick_class=None, split_screen=False): @@ -413,7 +426,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() + BrickStore.set_last_saved() @classmethod def add_namespace(cls, alias, uri): @@ -427,11 +440,7 @@ 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: @@ -512,7 +521,7 @@ class BrickStore: ignore_namespace = True break if not ignore_namespace: - BrickStore.namespaces.append((uri, f"{alias}: {uri}", "")) + BrickStore.namespaces.append((alias, str(uri))) @classmethod def load_entity_classes(cls): @@ -534,7 +543,7 @@ class BrickStore: ) BrickStore.entity_classes[root_class] = [] for uri in sorted([x[0].toPython() for x in query]): - BrickStore.entity_classes[root_class].append((uri, uri.split("#")[-1], "")) + BrickStore.entity_classes[root_class].append(uri) @classmethod def load_relationships(cls): @@ -548,7 +557,7 @@ class BrickStore: """ ) for uri in sorted([x[0].toPython() for x in query]): - BrickStore.relationships.append((uri, uri.split("#")[-1], "")) + BrickStore.relationships.append(uri) @classmethod def set_history_size(cls, size): @@ -590,3 +599,9 @@ class BrickStore: for i in range(0, total_changesets): BrickStore.graph.redo() BrickStore.history.append(total_changesets) + + @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}" \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 008e1667a8..aa3af91100 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -475,7 +475,7 @@ class Drawing(blenderbim.core.tool.Drawing): 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))) + 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": return mathutils.Matrix(((-1, 0, 0, x), (0, 0, 1, y), (0, 1, 0, z), (0, 0, 0, 1))) @@ -1504,7 +1504,7 @@ class Drawing(blenderbim.core.tool.Drawing): elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects) include = pset.get("Include", None) if include: - elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements)) + elements = ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements) else: if tool.Ifc.get_schema() == "IFC2X3": base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement")) @@ -1516,7 +1516,7 @@ class Drawing(blenderbim.core.tool.Drawing): exclude = pset.get("Exclude", None) if exclude: - elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, exclude, elements=elements)) + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) elements -= set(ifc_file.by_type("IfcOpeningElement")) return elements @@ -1529,10 +1529,10 @@ class Drawing(blenderbim.core.tool.Drawing): tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")] ) if include: - elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements)) + elements = ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements) exclude = pset.get("Exclude", None) if exclude: - elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, exclude, elements=elements)) + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) return elements @classmethod diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index fe57aa6a76..3b78dfb8d4 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -396,8 +396,18 @@ class Geometry(blenderbim.core.tool.Geometry): return f"{representation.ContextOfItems.id()}/{representation.id()}" @classmethod - def get_styles(cls, obj): - return [tool.Style.get_style(s.material) for s in obj.material_slots if s.material] + def get_styles(cls, obj, only_assigned_to_faces=False): + styles = [tool.Style.get_style(s.material) for s in obj.material_slots if s.material] + if not only_assigned_to_faces: + return styles + + usage_count = [0] * len(obj.material_slots) + if not usage_count: # if there are no materials, polygons will still use index 0 + return [] + for poly in obj.data.polygons: + usage_count[poly.material_index] += 1 + styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0] + return styles # TODO: multiple Literals? @classmethod diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index ea41bfbe6c..3c5698505d 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -32,7 +32,7 @@ import ifcopenshell.util.date as ifcdateutils import ifcopenshell.util.cost import ifcopenshell.util.resource import blenderbim.bim.schema - +import ifcopenshell.util.constraint class Resource(blenderbim.core.tool.Resource): @classmethod @@ -55,12 +55,13 @@ class Resource(blenderbim.core.tool.Resource): tprops = bpy.context.scene.BIMResourceTreeProperties tprops.resources.clear() contracted_resources = json.loads(props.contracted_resources) - + props.is_resource_update_enabled = False for resource in tool.Ifc.get().by_type("IfcResource"): if not resource.HasContext: continue create_new_resource_li(resource, 0) cls.load_productivity_data() + props.is_resource_update_enabled = True props.is_editing = True @classmethod @@ -71,7 +72,7 @@ class Resource(blenderbim.core.tool.Resource): for item in tprops.resources: resource = tool.Ifc.get().by_id(item.ifc_definition_id) item.name = resource.Name if resource.Name else "Unnamed" - item.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0 + item.schedule_usage = resource.Usage.ScheduleUsage if (resource.Usage and resource.Usage.ScheduleUsage) else 0 props.is_resource_update_enabled = True @classmethod @@ -279,6 +280,8 @@ class Resource(blenderbim.core.tool.Resource): def expand_resource(cls, resource): props = bpy.context.scene.BIMResourceProperties contracted_resources = json.loads(props.contracted_resources) + if not resource.id() in contracted_resources: + return contracted_resources.remove(resource.id()) props.contracted_resources = json.dumps(contracted_resources) @@ -372,12 +375,75 @@ class Resource(blenderbim.core.tool.Resource): @classmethod def edit_productivity_pset(cls, resource, attributes): productivity = cls.get_productivity(resource) - if productivity: - pset = tool.Ifc.get().by_id(productivity["id"]) - else: - pset = tool.Ifc.run("pset.add_pset", product=resource, name="EPset_Productivity") - tool.Ifc.run( + if not productivity: + return + return tool.Ifc.run( "pset.edit_pset", - pset=pset, + pset= tool.Ifc.get().by_id(productivity["id"]), properties=attributes, - ) \ No newline at end of file + ) + + @classmethod + def get_constraints(cls, resource): + return ifcopenshell.util.constraint.get_constraints(product=resource) + + @classmethod + def get_metrics(cls, constraint): + return ifcopenshell.util.constraint.get_metrics(constraint) + + @classmethod + def get_metric_reference(cls, metric, is_deep=True): + return ifcopenshell.util.constraint.get_metric_reference(metric, is_deep=is_deep) + + @classmethod + def has_metric_constraint(cls, resource, attribute): + metrics = ifcopenshell.util.constraint.get_metric_constraints(resource, attribute) + return True if metrics else False + + @classmethod + def run_edit_resource_time(cls, resource, attributes): + if not resource.Usage: + tool.Ifc.run( + "resource.add_resource_time", + resource=resource, + ) + tool.Ifc.run("resource.edit_resource_time", resource_time=resource.Usage, attributes=attributes) + + @classmethod + def go_to_resource(cls, resource): + def get_ancestors_ids(resource): + ids = [] + for rel in resource.Nests or []: + ids.append(rel.RelatingObject.id()) + ids.extend(get_ancestors_ids(rel.RelatingObject)) + return ids + + ancestors = get_ancestors_ids(resource) + contracted_resources = json.loads(bpy.context.scene.BIMResourceProperties.contracted_resources) + for ancestor in ancestors: + if ancestor in contracted_resources: + contracted_resources.remove(ancestor) + bpy.context.scene.BIMResourceProperties.contracted_resources = json.dumps(contracted_resources) + cls.load_resources() + cls.load_resource_properties() + + resource_props = bpy.context.scene.BIMResourceTreeProperties + expanded_resources = [item.ifc_definition_id for item in resource_props.resources] + bpy.context.scene.BIMResourceProperties.active_resource_index = expanded_resources.index(resource.id()) + + + @classmethod + def run_calculate_resource_usage(cls, resource): + tool.Ifc.run("resource.calculate_resource_usage", resource=resource) + + @classmethod + def get_task_assignments(cls, resource): + return ifcopenshell.util.resource.get_task_assignments(resource) + + @classmethod + def get_nested_resources(cls, resource): + return ifcopenshell.util.resource.get_nested_resources(resource) + + @classmethod + def is_attribute_locked(cls, resource, attribute): + return ifcopenshell.util.constraint.is_attribute_locked(resource, attribute) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/search.py b/src/blenderbim/blenderbim/tool/search.py index 49c5b978ef..9eeb17eff2 100644 --- a/src/blenderbim/blenderbim/tool/search.py +++ b/src/blenderbim/blenderbim/tool/search.py @@ -184,7 +184,7 @@ class ImportFilterQueryTransformer(lark.Transformer): elif args[0].data == "quoted_string": return args[0].children[0].value[1:-1].replace('\\"', '"') elif args[0].data == "regex_string": - return args[0].children[0].value + return "/" + args[0].children[0].value + "/" elif args[0].data == "special": if args[0].children[0].data == "null": return "NULL" diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index ebd84b656c..6fea1e3639 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -198,8 +198,12 @@ class Sequence(blenderbim.core.tool.Sequence): item.name = task.Name or "Unnamed" item.identification = task.Identification or "XXX" if props.highlighted_task_id: - item.is_predecessor = props.highlighted_task_id in [rel.RelatedProcess.id() for rel in task.IsPredecessorTo] - item.is_successor = props.highlighted_task_id in [rel.RelatingProcess.id() for rel in task.IsSuccessorFrom] + item.is_predecessor = props.highlighted_task_id in [ + rel.RelatedProcess.id() for rel in task.IsPredecessorTo + ] + item.is_successor = props.highlighted_task_id in [ + rel.RelatingProcess.id() for rel in task.IsSuccessorFrom + ] calendar = ifcopenshell.util.sequence.derive_calendar(task) if task.HasAssignments: for rel in task.HasAssignments: @@ -412,11 +416,12 @@ class Sequence(blenderbim.core.tool.Sequence): new = props.task_resources.add() new.ifc_definition_id = resource.id() new.name = resource.Name or "Unnamed" - new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0 + new.schedule_usage = resource.Usage.ScheduleUsage or 0 if resource.Usage else 0 @classmethod def load_resources(cls): blenderbim.core.resource.load_resources(tool.Resource) + cls.refresh_task_resources @classmethod def get_task_inputs(cls, task): @@ -466,9 +471,11 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def get_highlighted_task(cls): - props = bpy.context.scene.BIMWorkScheduleProperties - task_props = bpy.context.scene.BIMTaskTreeProperties - return tool.Ifc.get().by_id(task_props.tasks[props.active_task_index].ifc_definition_id) + tasks = bpy.context.scene.BIMTaskTreeProperties.tasks + if len(tasks) and len(tasks) > bpy.context.scene.BIMWorkScheduleProperties.active_task_index: + return tool.Ifc.get().by_id( + tasks[bpy.context.scene.BIMWorkScheduleProperties.active_task_index].ifc_definition_id + ) @classmethod def get_direct_nested_tasks(cls, task): @@ -483,15 +490,6 @@ class Sequence(blenderbim.core.tool.Sequence): is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs return ifcopenshell.util.sequence.get_task_outputs(task, is_deep) - @classmethod - def get_task_resources(cls, task): - resources = [] - for rel in task.OperatesOn: - for object in rel.RelatedObjects: - if object.is_a("IfcResource"): - resources.append(object) - return resources - @classmethod def enable_editing_work_calendar_times(cls, work_calendar): props = bpy.context.scene.BIMWorkCalendarProperties @@ -768,25 +766,27 @@ class Sequence(blenderbim.core.tool.Sequence): ) @classmethod - def highlight_task(cls, task): - def expand_ancestors(task): + def go_to_task(cls, task): + def get_ancestor_ids(task): + ids = [] for rel in task.Nests or []: - parent_task = rel.RelatingObject if rel.RelatingObject.is_a("IfcTask") else None - contracted_tasks = json.loads(bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks) - if parent_task and parent_task.id() in contracted_tasks: - contracted_tasks.remove(parent_task.id()) - bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks = json.dumps(contracted_tasks) - expand_ancestors(parent_task) - work_schedule = cls.get_active_work_schedule() - cls.load_task_tree(work_schedule) - cls.load_task_properties() + ids.append(rel.RelatingObject.id()) + ids.extend(get_ancestor_ids(rel.RelatingObject)) + return ids + + contracted_tasks = json.loads(bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks) + for ancestor_id in get_ancestor_ids(task): + if ancestor_id in contracted_tasks: + contracted_tasks.remove(ancestor_id) + bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks = json.dumps(contracted_tasks) + + work_schedule = cls.get_active_work_schedule() + cls.load_task_tree(work_schedule) + cls.load_task_properties() task_props = bpy.context.scene.BIMTaskTreeProperties - displayed_tasks = [item.ifc_definition_id for item in task_props.tasks] - if not task.id() in displayed_tasks: - expand_ancestors(task) - task_index = displayed_tasks.index(task.id()) or 0 - bpy.context.scene.BIMWorkScheduleProperties.active_task_index = task_index + expanded_tasks = [item.ifc_definition_id for item in task_props.tasks] + bpy.context.scene.BIMWorkScheduleProperties.active_task_index = expanded_tasks.index(task.id()) or 0 @classmethod def guess_date_range(cls, work_schedule): @@ -1667,4 +1667,17 @@ class Sequence(blenderbim.core.tool.Sequence): resources = cls.get_task_resources(task) cls.load_task_inputs(inputs) cls.load_task_outputs(outputs) - cls.load_task_resources(resources) \ No newline at end of file + cls.load_task_resources(resources) + + @classmethod + def refresh_task_resources(cls): + task = cls.get_highlighted_task() + if not task: + return + cls.load_task_resources(cls.get_task_resources(task)) + + @classmethod + def has_duration(cls, task): + if task.TaskTime and task.TaskTime.ScheduleDuration: + return True + return False diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 3bb15d5cb0..b274ff2929 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -22,12 +22,12 @@ import blenderbim.core.tool import blenderbim.tool as tool from blenderbim.bim import import_ifc import re -from mathutils import Matrix +from mathutils import Matrix, Vector class System(blenderbim.core.tool.System): @classmethod - def add_ports(cls, obj, add_start_port=True, add_end_port=True): + def add_ports(cls, obj, add_start_port=True, add_end_port=True, offset_end_port=None): def add_port(mep_element, matrix): port = tool.Ifc.run("system.add_port", element=mep_element) port.FlowDirection = "NOTDEFINED" @@ -47,7 +47,10 @@ class System(blenderbim.core.tool.System): 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)))) + m = obj.matrix_world @ Matrix.Translation((0, 0, length)) + if offset_end_port: + m.translation += offset_end_port + ports.append(add_port(mep_element, m)) return ports @classmethod diff --git a/src/blenderbim/scripts/setup_pytest.py b/src/blenderbim/scripts/setup_pytest.py index 2e2f6eea51..8bfad2dfb0 100644 --- a/src/blenderbim/scripts/setup_pytest.py +++ b/src/blenderbim/scripts/setup_pytest.py @@ -32,16 +32,16 @@ subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"]) sys_paths = [p for p in sys.path if "site-packages" in p] if sys_paths: print("Detected installation directory:", sys_paths[-1]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest"]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-bdd"]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-blender"]) - subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pygments"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pytest"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pytest-bdd"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pytest-blender"]) + subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "--upgrade", "pygments"]) else: print("Could not detect installation directory. Good luck.") - subprocess.call([py_exec, "-m", "pip", "install", "pytest"]) - subprocess.call([py_exec, "-m", "pip", "install", "pytest-bdd"]) - subprocess.call([py_exec, "-m", "pip", "install", "pytest-blender"]) - subprocess.call([py_exec, "-m", "pip", "install", "pygments"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pytest"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pytest-bdd"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pytest-blender"]) + subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pygments"]) try: import pytest diff --git a/src/blenderbim/test/bim/feature/brick.feature b/src/blenderbim/test/bim/feature/brick.feature index d7a86cab5e..06fcfbdefd 100644 --- a/src/blenderbim/test/bim/feature/brick.feature +++ b/src/blenderbim/test/bim/feature/brick.feature @@ -1,114 +1,203 @@ @brick Feature: Brick +Scenario: Create Brick project + Given an empty Blender session + And the Brickschema is stubbed + When I press "bim.new_brick_file" + Then nothing happens + Scenario: Load Brick project Given an empty Blender session + And the Brickschema is stubbed When I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" Then nothing happens Scenario: View Brick class Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - When I press "bim.view_brick_class(brick_class='Equipment')" + When I press "bim.view_brick_class(brick_class='Building')" Then nothing happens Scenario: View Brick item Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - When I press "bim.view_brick_item(item='https://brickschema.org/schema/Brick#Chiller')" + When I press "bim.view_brick_item(item='https://example.org/digitaltwin#lighting_zone_1')" Then nothing happens -Scenario: Rewind brick class +Scenario: Rewind Brick class Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I press "bim.view_brick_class(brick_class='Equipment')" - When I press "bim.rewind_brick_class()" + And I press "bim.view_brick_class(brick_class='Building')" + When I press "bim.rewind_brick_class" Then nothing happens Scenario: Close Brick project Given an empty Blender session + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.close_brick_project" Then nothing happens +Scenario: Close Brick project then create Brick project + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.close_brick_project" + And the Brickschema is stubbed + When I press "bim.new_brick_file" + Then nothing happens + +Scenario: Add Brick - vanilla Brick with no IFC + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.new_brick_file" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" + When I press "bim.add_brick" + Then nothing happens + +Scenario: Add Brick - from geometry without a Brick IFC library + Given an empty IFC project + And the Brickschema is stubbed + And I press "bim.new_brick_file" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + When I press "bim.add_brick" + Then nothing happens + +Scenario: Add Brick - from geometry with a Brick IFC library + Given an empty IFC project + And the Brickschema is stubbed + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.convert_brick_project" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + When I press "bim.add_brick" + Then nothing happens + +Scenario: Refresh Brick viewer + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.new_brick_file" + When I press "bim.refresh_brick_viewer" + Then nothing happens + +Scenario: Add Brick relation - vanilla Brick with no IFC + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I press "bim.view_brick_class(brick_class='Lighting_Zone')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + And I set "scene.BIMBrickProperties.brick_create_relations_toggled" to "True" + And I set "scene.BIMBrickProperties.new_brick_relation_object" to "xyz789" + When I press "bim.add_brick_relation" + Then nothing happens + +Scenario: Add Brick relation - vanilla Brick with no IFC and with split screen + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMBrickProperties.brick_entity_create_type" to "Location" + And I set "scene.BIMBrickProperties.new_brick_label" to "abc123" + And I set "scene.BIMBrickProperties.brick_entity_class" to "https://brickschema.org/schema/Brick#Building" + And I press "bim.add_brick" + And I press "bim.view_brick_class(brick_class='Lighting_Zone')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + And I set "scene.BIMBrickProperties.split_screen_toggled" to "True" + And I press "bim.view_brick_class(brick_class='Building', split_screen=True)" + And I set "scene.BIMBrickProperties.split_screen_active_brick_index" to "0" + And I set "scene.BIMBrickProperties.brick_create_relations_toggled" to "True" + When I press "bim.add_brick_relation" + Then nothing happens + +Scenario: Remove Brick - vanilla Brick + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.view_brick_class(brick_class='Lighting_Zone')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + When I press "bim.remove_brick" + Then nothing happens + +Scenario: Remove Brick - with a Brick IFC library reference + Given an empty IFC project + And the Brickschema is stubbed + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I press "bim.add_brick" + And I press "bim.view_brick_class(brick_class='Equipment')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" + When I press "bim.remove_brick" + Then nothing happens + +Scenario: Change viewer list root + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.set_list_root_toggled" to "True" + When I set "scene.BIMBrickProperties.brick_list_root" to "Location" + Then nothing happens + +Scenario: Change viewer list root - split screen + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.set_list_root_toggled" to "True" + And I set "scene.BIMBrickProperties.split_screen_toggled" to "True" + When I set "scene.BIMBrickProperties.split_screen_brick_list_root" to "Location" + Then nothing happens + +Scenario: Toggle split screen + Given an empty Blender session + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + When I set "scene.BIMBrickProperties.split_screen_toggled" to "True" + Then nothing happens + +Scenario: Set active namespace + Given an empty Blender session + And the Brickschema is stubbed + When I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + Then nothing happens + +Scenario: Bind new namespace + Given an empty Blender session + And the Brickschema is stubbed + And I set "scene.BIMBrickProperties.new_brick_namespace_alias" to "digitaltwin2" + And I set "scene.BIMBrickProperties.new_brick_namespace_uri" to "https://example.org/digitaltwin2#" + When I press "bim.add_brick_namespace" + Then nothing happens + Scenario: Convert brick project Given an empty IFC project + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.convert_brick_project" Then nothing happens -Scenario: Assign brick reference - Given an empty IFC project - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - And I set "scene.BIMBrickProperties.brick_equipment_class" to "https://brickschema.org/schema/Brick#Chiller" - And I press "bim.add_brick" - And I press "bim.view_brick_class(brick_class='Chiller')" - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" - And I press "bim.assign_class" - And the object "IfcChiller/Cube" is selected - And I press "bim.convert_brick_project" - When I press "bim.assign_brick_reference" - Then nothing happens - -Scenario: Add brick - vanilla brick with no IFC - Given an empty Blender session - And I press "bim.new_brick_file" - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - When I press "bim.add_brick" - Then nothing happens - -Scenario: Add brick - without a brick IFC library - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" - And I press "bim.assign_class" - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And the object "IfcChiller/Cube" is selected - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - When I press "bim.add_brick" - Then nothing happens - -Scenario: Add brick - with a brick IFC library - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" - And I press "bim.assign_class" - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I press "bim.convert_brick_project" - And the object "IfcChiller/Cube" is selected - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - When I press "bim.add_brick" - Then nothing happens - -Scenario: Add brick feed - Given an empty IFC project - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" - And I press "bim.convert_brick_project" - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcUnitaryEquipment" - And I set "scene.BIMRootProperties.ifc_predefined_type" to "AIRHANDLER" - And I press "bim.assign_class" - And I press "bim.add_brick" - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcAirTerminalBox" - And I set "scene.BIMRootProperties.ifc_predefined_type" to "VARIABLEFLOWPRESSUREDEPENDANT" - And I press "bim.assign_class" - And I press "bim.add_brick" - And the object "IfcUnitaryEquipment/Cube" is selected - And additionally the object "IfcAirTerminalBox/Cube" is selected - When I press "bim.add_brick_feed" - Then nothing happens - Scenario: Convert IFC to brick Given an empty IFC project + And the Brickschema is stubbed And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And I press "bim.convert_brick_project" And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" @@ -120,26 +209,20 @@ Scenario: Convert IFC to brick When I press "bim.convert_ifc_to_brick" Then nothing happens -Scenario: New brick file - Given an empty Blender session - When I press "bim.new_brick_file" - Then nothing happens - -Scenario: Refresh brick viewer - Given an empty Blender session - And I press "bim.new_brick_file" - When I press "bim.refresh_brick_viewer" - Then nothing happens - -Scenario: Remove brick - without a brick IFC library +Scenario: Assign brick reference Given an empty IFC project + And the Brickschema is stubbed + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" + And I set "scene.BIMBrickProperties.brick_entity_class" to "https://brickschema.org/schema/Brick#Chiller" + And I press "bim.add_brick" + And I press "bim.view_brick_class(brick_class='Chiller')" + And I set "scene.BIMBrickProperties.active_brick_index" to "0" And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" And I press "bim.assign_class" - And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" And the object "IfcChiller/Cube" is selected - And I set "scene.BIMBrickProperties.namespace" to "https://example.org/digitaltwin#" - And I press "bim.add_brick" - When I press "bim.remove_brick" + And I press "bim.convert_brick_project" + When I press "bim.assign_brick_reference" Then nothing happens diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 54381e95fe..fc9afdd286 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -47,6 +47,7 @@ Scenario: Add representation - add a representation with a scale factor applied And I add a cube And the object "Cube" is selected When the object "Cube" is scaled to "2" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -58,6 +59,7 @@ Scenario: Add representation - add a representation with a scale factor removed And I press "object.duplicate_move_linked" And the object "Cube" is selected When the object "Cube" is scaled to "2" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -179,6 +181,7 @@ Scenario: Update representation - updating a tessellation Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.update_representation(obj='IfcWall/Cube')" @@ -188,6 +191,7 @@ Scenario: Update representation - updating a layered extrusion Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -318,7 +322,7 @@ Scenario: Override duplicate move - copying a coloured representation When I duplicate the selected objects And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started - And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" Then the material "Material" colour is "1,0,0,1" And the object "IfcWall/Cube" has the material "Material" And the object "IfcWall/Cube.001" has the material "Material" diff --git a/src/blenderbim/test/bim/feature/misc.feature b/src/blenderbim/test/bim/feature/misc.feature index 6f08795603..282ec0d16b 100644 --- a/src/blenderbim/test/bim/feature/misc.feature +++ b/src/blenderbim/test/bim/feature/misc.feature @@ -48,3 +48,10 @@ Scenario: Split along edge When I press "bim.split_along_edge" Then the object "IfcWall/Cube" is an "IfcWall" And the object "IfcWall/Cube.001" is an "IfcWall" + +Scenario: Enabling and disabling IFC Sverchok + Given an empty IFC project + And I press "preferences.addon_enable(module="sverchok")" + And I press "preferences.addon_enable(module="ifcsverchok")" + And I press "preferences.addon_disable(module="sverchok")" + And I press "preferences.addon_disable(module="ifcsverchok")" \ No newline at end of file diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature index b44228a69c..d61d895b43 100644 --- a/src/blenderbim/test/bim/feature/project.feature +++ b/src/blenderbim/test/bim/feature/project.feature @@ -456,7 +456,7 @@ Scenario: Export IFC - with changed style colour synchronised When the material "Material" colour is set to "1,0,0,1" And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started - And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" Then the material "Material" colour is "1,0,0,1" Scenario: Export IFC - with changed style element synchronised @@ -471,5 +471,5 @@ Scenario: Export IFC - with changed style element synchronised And the material "Material.001" colour is set to "1,0,0,1" And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started - And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" Then the material "Material.001" colour is "1,0,0,1" diff --git a/src/blenderbim/test/bim/feature/resource.feature b/src/blenderbim/test/bim/feature/resource.feature index 836639f16f..3e01cfa926 100644 --- a/src/blenderbim/test/bim/feature/resource.feature +++ b/src/blenderbim/test/bim/feature/resource.feature @@ -249,7 +249,8 @@ Scenario: Add Productivity data When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" - And I set "scene.BIMResourceProperties.should_show_productivity" to "True" + And I press "bim.add_productivity_data" + And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00" And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5" @@ -286,7 +287,8 @@ Scenario: Calculate Resource Work When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" - And I set "scene.BIMResourceProperties.should_show_productivity" to "True" + And I press "bim.add_productivity_data" + And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00" And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index a5e073d510..9fd1c4317f 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -24,6 +24,7 @@ import ifcopenshell import blenderbim.tool as tool import blenderbim.bim from blenderbim.bim.ifc import IfcStore +from blenderbim.tool.brick import BrickStore from blenderbim.bim.module.model.data import AuthoringData from pytest_bdd import scenarios, given, when, then, parsers from mathutils import Vector @@ -71,6 +72,8 @@ def an_empty_blender_session(): if len(bpy.data.objects) > 0: bpy.data.batch_remove(bpy.data.objects) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + if len(bpy.data.materials) > 0: + bpy.data.batch_remove(bpy.data.materials) # default project settings bpy.context.scene.unit_settings.system = "METRIC" @@ -92,6 +95,13 @@ def an_empty_ifc_2x3_project(): bpy.ops.bim.create_project() +@given("the Brickschema is stubbed") +def the_brickschema_is_stubbed(): + # This makes things run faster since we don't need to load the entire brick schema + cwd = os.path.dirname(os.path.realpath(__file__)) + BrickStore.schema = os.path.join(cwd, "..", "files", "BrickStub.ttl") + + @when("I load a new pset template file") def i_load_a_new_pset_template_file(): IfcStore.pset_template_path = bpy.context.scene.BIMPsetTemplateProperties.pset_template_files diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index 2b5eb70855..157a363682 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -23,46 +23,80 @@ from test.core.bootstrap import ifc, brick class TestLoadBrickProject: def test_run(self, brick): brick.load_brick_file("filepath").should_be_called() - brick.import_brick_classes("Class").should_be_called() - brick.set_active_brick_class("Class").should_be_called() - subject.load_brick_project(brick, filepath="filepath") + brick.import_brick_classes("brick_root").should_be_called() + brick.import_brick_classes("brick_root", split_screen=True).should_be_called() + brick.set_active_brick_class("brick_root").should_be_called() + brick.set_active_brick_class("brick_root", split_screen=True).should_be_called() + subject.load_brick_project(brick, filepath="filepath", brick_root="brick_root") + + +class TestNewBrickFile: + def test_run(self, brick): + brick.new_brick_file().should_be_called() + brick.import_brick_classes("brick_root").should_be_called() + brick.import_brick_classes("brick_root", split_screen=True).should_be_called() + brick.set_active_brick_class("brick_root").should_be_called() + brick.set_active_brick_class("brick_root", split_screen=True).should_be_called() + subject.new_brick_file(brick, brick_root="brick_root") class TestViewBrickClass: def test_run(self, brick): - brick.add_brick_breadcrumb().should_be_called() - brick.clear_brick_browser().should_be_called() - brick.import_brick_classes("brick_class").should_be_called() - brick.import_brick_items("brick_class").should_be_called() - brick.set_active_brick_class("brick_class").should_be_called() - subject.view_brick_class(brick, brick_class="brick_class") + brick.add_brick_breadcrumb(split_screen=False).should_be_called() + brick.clear_brick_browser(split_screen=False).should_be_called() + brick.import_brick_classes("brick_class", split_screen=False).should_be_called() + brick.import_brick_items("brick_class", split_screen=False).should_be_called() + brick.set_active_brick_class("brick_class", split_screen=False).should_be_called() + subject.view_brick_class(brick, brick_class="brick_class", split_screen=False) + + def test_split_screen(self, brick): + brick.add_brick_breadcrumb(split_screen=True).should_be_called() + brick.clear_brick_browser(split_screen=True).should_be_called() + brick.import_brick_classes("brick_class", split_screen=True).should_be_called() + brick.import_brick_items("brick_class", split_screen=True).should_be_called() + brick.set_active_brick_class("brick_class", split_screen=True).should_be_called() + subject.view_brick_class(brick, brick_class="brick_class", split_screen=True) class TestViewBrickItem: def test_run(self, brick): - brick.add_brick_breadcrumb().should_be_called() - brick.clear_brick_browser().should_be_called() brick.get_item_class("item").should_be_called().will_return("brick_class") - brick.import_brick_classes("brick_class").should_be_called() - brick.import_brick_items("brick_class").should_be_called() - brick.set_active_brick_class("brick_class").should_be_called() - brick.select_browser_item("item").should_be_called() - subject.view_brick_item(brick, item="item") + brick.run_view_brick_class(brick_class="brick_class", split_screen=False).should_be_called() + brick.select_browser_item("item", split_screen=False).should_be_called() + subject.view_brick_item(brick, item="item", split_screen=False) + + def test_split_screen(self, brick): + brick.get_item_class("item").should_be_called().will_return("brick_class") + brick.run_view_brick_class(brick_class="brick_class", split_screen=True).should_be_called() + brick.select_browser_item("item", split_screen=True).should_be_called() + subject.view_brick_item(brick, item="item", split_screen=True) class TestRewindBrickClass: def test_run(self, brick): - brick.pop_brick_breadcrumb().should_be_called().will_return("previous_class") - brick.clear_brick_browser().should_be_called() - brick.import_brick_classes("previous_class").should_be_called() - brick.import_brick_items("previous_class").should_be_called() - brick.set_active_brick_class("previous_class").should_be_called() - subject.rewind_brick_class(brick) + brick.pop_brick_breadcrumb(split_screen=False).should_be_called().will_return("previous_class") + brick.clear_brick_browser(split_screen=False).should_be_called() + brick.import_brick_classes("previous_class", split_screen=False).should_be_called() + brick.import_brick_items("previous_class", split_screen=False).should_be_called() + brick.set_active_brick_class("previous_class", split_screen=False).should_be_called() + subject.rewind_brick_class(brick, split_screen=False) + + def test_split_screen(self, brick): + brick.pop_brick_breadcrumb(split_screen=True).should_be_called().will_return("previous_class") + brick.clear_brick_browser(split_screen=True).should_be_called() + brick.import_brick_classes("previous_class", split_screen=True).should_be_called() + brick.import_brick_items("previous_class", split_screen=True).should_be_called() + brick.set_active_brick_class("previous_class", split_screen=True).should_be_called() + subject.rewind_brick_class(brick, split_screen=True) class TestCloseBrickProject: def test_run(self, brick): brick.clear_project().should_be_called() + brick.clear_brick_browser().should_be_called() + brick.clear_brick_browser(split_screen=True).should_be_called() + brick.clear_breadcrumbs().should_be_called() + brick.clear_breadcrumbs(split_screen=True).should_be_called() subject.close_brick_project(brick) @@ -86,84 +120,113 @@ class TestConvertBrickProject: class TestAssignBrickReference: def test_assigning_to_a_new_reference(self, ifc, brick): - brick.get_library_brick_reference("library", "brick").should_be_called().will_return(None) + brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return(None) ifc.run("library.add_reference", library="library").should_be_called().will_return("reference") - brick.export_brick_attributes("brick").should_be_called().will_return("attributes") + brick.export_brick_attributes("brick_uri").should_be_called().will_return("attributes") ifc.run("library.edit_reference", reference="reference", attributes="attributes").should_be_called() ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") - brick.add_brickifc_reference("brick", "element", "project").should_be_called() - subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick") + brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() + subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_assigning_to_an_existing_reference(self, ifc, brick): - brick.get_library_brick_reference("library", "brick").should_be_called().will_return("reference") + brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") - brick.add_brickifc_reference("brick", "element", "project").should_be_called() - subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick") + brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() + subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_adding_a_brickifc_project_if_it_doesnt_exist(self, ifc, brick): - brick.get_library_brick_reference("library", "brick").should_be_called().will_return("reference") + brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return(None) - brick.get_namespace("brick").should_be_called().will_return("namespace") + brick.get_namespace("brick_uri").should_be_called().will_return("namespace") brick.add_brickifc_project("namespace").should_be_called().will_return("project") - brick.add_brickifc_reference("brick", "element", "project").should_be_called() - subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick") + brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() + subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") class TestAddBrick: def test_adding_a_brick_from_an_element(self, ifc, brick): brick.add_brick_from_element("element", "namespace", "brick_class").should_be_called().will_return("brick_uri") brick.run_refresh_brick_viewer().should_be_called() - subject.add_brick(ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library=None) + subject.add_brick( + ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library=None, label="label" + ) - def test_adding_a_brick_an_auto_assigning_it_to_the_ifc_element(self, ifc, brick): + def test_adding_a_brick_and_auto_assigning_it_to_the_ifc_element(self, ifc, brick): brick.add_brick_from_element("element", "namespace", "brick_class").should_be_called().will_return("brick_uri") brick.run_assign_brick_reference(element="element", library="library", brick_uri="brick_uri").should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.add_brick( - ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library="library" + ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library="library", label="label" ) def test_adding_a_plain_brick(self, ifc, brick): - brick.add_brick("namespace", "brick_class").should_be_called() + brick.add_brick("namespace", "brick_class", "label").should_be_called() brick.run_refresh_brick_viewer().should_be_called() - subject.add_brick(ifc, brick, element=None, namespace="namespace", brick_class="brick_class", library=None) + subject.add_brick(ifc, brick, element=None, namespace="namespace", brick_class="brick_class", library=None, label="label") -class TestAddBrickFeed: +class TestAddBrickRelation: def test_run(self, ifc, brick): - brick.get_brick("source").should_be_called().will_return("source_brick") - brick.get_brick("destination").should_be_called().will_return("destination_brick") - brick.add_feed("source_brick", "destination_brick").should_be_called() + brick.add_relation("brick_uri", "predicate", "object").should_be_called() brick.run_refresh_brick_viewer().should_be_called() - subject.add_brick_feed(ifc, brick, source="source", destination="destination") + subject.add_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") class TestConvertIfcToBrick: def test_run(self, brick): - brick.get_convertable_brick_elements().should_be_called().will_return(["element"]) - brick.get_brick_class("element").should_be_called().will_return("brick_class") - brick.add_brick_from_element("element", "namespace", "brick_class").should_be_called().will_return("brick_uri") - brick.run_assign_brick_reference(element="element", library="library", brick_uri="brick_uri").should_be_called() + brick.get_convertable_brick_spaces().should_be_called().will_return({"space", "parent"}) + brick.get_brick_class("space").should_be_called().will_return("space_class") + brick.add_brick_from_element("space", "namespace", "space_class").should_be_called().will_return("space_uri") + brick.run_assign_brick_reference(element="space", library="library", brick_uri="space_uri").should_be_called() + + brick.get_brick_class("parent").should_be_called().will_return("parent_class") + brick.add_brick_from_element("parent", "namespace", "parent_class").should_be_called().will_return("parent_uri") + brick.run_assign_brick_reference(element="parent", library="library", brick_uri="parent_uri").should_be_called() + + brick.get_parent_space("space").should_be_called().will_return("parent") + brick.get_parent_space("parent").should_be_called().will_return(None) + brick.add_relation("parent_uri", "https://brickschema.org/schema/Brick#hasPart", "space_uri").should_be_called() + + brick.get_convertable_brick_systems().should_be_called().will_return({"system"}) + brick.get_brick_class("system").should_be_called().will_return("system_class") + brick.add_brick_from_element("system", "namespace", "system_class").should_be_called().will_return("system_uri") + brick.run_assign_brick_reference(element="system", library="library", brick_uri="system_uri").should_be_called() + + brick.get_convertable_brick_elements().should_be_called().will_return({"element", "downstream_element"}) + brick.get_brick_class("element").should_be_called().will_return("element_class") + brick.add_brick_from_element("element", "namespace", "element_class").should_be_called().will_return("element_uri") + brick.get_element_container("element").should_be_called().will_return("space") + brick.add_relation("element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri").should_be_called() + brick.get_element_systems("element").should_be_called().will_return(["system"]) + brick.add_relation("system_uri", "https://brickschema.org/schema/Brick#hasPart", "element_uri").should_be_called() + brick.run_assign_brick_reference(element="element", library="library", brick_uri="element_uri").should_be_called() + + brick.get_brick_class("downstream_element").should_be_called().will_return("downstream_element_class") + brick.add_brick_from_element("downstream_element", "namespace", "downstream_element_class").should_be_called().will_return("downstream_element_uri") + brick.get_element_container("downstream_element").should_be_called().will_return("space") + brick.add_relation("downstream_element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri").should_be_called() + brick.get_element_systems("downstream_element").should_be_called().will_return(["system"]) + brick.add_relation("system_uri", "https://brickschema.org/schema/Brick#hasPart", "downstream_element_uri").should_be_called() + brick.run_assign_brick_reference(element="downstream_element", library="library", brick_uri="downstream_element_uri").should_be_called() + + brick.get_element_feeds("element").should_be_called().will_return(["downstream_element"]) + brick.get_element_feeds("downstream_element").should_be_called().will_return([]) + brick.add_relation("element_uri", "https://brickschema.org/schema/Brick#feeds", "downstream_element_uri").should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.convert_ifc_to_brick(brick, namespace="namespace", library="library") -class TestNewBrickFile: - def test_run(self, brick): - brick.new_brick_file().should_be_called() - brick.import_brick_classes("Class").should_be_called() - brick.set_active_brick_class("Class").should_be_called() - subject.new_brick_file(brick) - - class TestRefreshBrickViewer: def test_run(self, brick): - brick.get_active_brick_class().should_be_called().will_return("class") - brick.run_view_brick_class(brick_class="class").should_be_called() + brick.get_active_brick_class().should_be_called().will_return("brick_class") + brick.run_view_brick_class(brick_class="brick_class").should_be_called() brick.pop_brick_breadcrumb().should_be_called() + brick.get_active_brick_class(split_screen=True).should_be_called().will_return("brick_class") + brick.run_view_brick_class(brick_class="brick_class", split_screen=True).should_be_called() + brick.pop_brick_breadcrumb(split_screen=True).should_be_called() subject.refresh_brick_viewer(brick) @@ -185,3 +248,33 @@ class TestRemoveBrick: brick.remove_brick("brick_uri").should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.remove_brick(ifc, brick, library=None, brick_uri="brick_uri") + + +class TestSerializeBrick: + def test_run(self, brick): + brick.serialize_brick().should_be_called() + subject.serialize_brick(brick) + + +class TestAddBrickNamespace: + def test_run(self, brick): + brick.add_namespace("alias", "uri").should_be_called() + subject.add_brick_namespace(brick, alias="alias", uri="uri") + + +class TestSetBrickListRoot: + def test_run(self, brick): + brick.run_view_brick_class(brick_class="brick_root", split_screen=False).should_be_called() + brick.clear_breadcrumbs(split_screen=False).should_be_called() + subject.set_brick_list_root(brick, brick_root="brick_root", split_screen=False) + + def test_split_screen(self, brick): + brick.run_view_brick_class(brick_class="brick_root", split_screen=True).should_be_called() + brick.clear_breadcrumbs(split_screen=True).should_be_called() + subject.set_brick_list_root(brick, brick_root="brick_root", split_screen=True) + + +class TestRemoveBrickRelation: + def test_run(self, brick): + brick.remove_relation("brick_uri", "predicate", "object").should_be_called() + subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") \ No newline at end of file diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index ba839a1869..cb5e76f248 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -490,7 +490,7 @@ class TestUpdateDrawingName: ) ifc.resolve_uri("relative_layout_uri").should_be_called().will_return("absolute_layout_uri") drawing.does_file_exist("absolute_layout_uri").should_be_called().will_return(True) - drawing.update_embedded_svg_location("absolute_layout_uri", "old_location", "new_location").should_be_called() + drawing.update_embedded_svg_location("absolute_layout_uri", "reference_with_old_location", "new_uri").should_be_called() drawing.is_editing_sheets().should_be_called().will_return(True) drawing.import_sheets().should_be_called() @@ -500,9 +500,9 @@ class TestUpdateDrawingName: class TestAddAnnotation: def test_run(self, ifc, collector, drawing): - drawing.show_decorations().should_be_called() drawing.get_drawing_target_view("drawing").should_be_called().will_return("target_view") drawing.get_annotation_context("target_view", "object_type").should_be_called().will_return("context") + drawing.show_decorations().should_be_called() drawing.create_annotation_object("drawing", "object_type").should_be_called().will_return("obj") ifc.get_entity("obj").should_be_called().will_return(None) drawing.get_ifc_representation_class("object_type").should_be_called().will_return("ifc_representation_class") @@ -520,7 +520,24 @@ class TestAddAnnotation: drawing.enable_editing("obj").should_be_called() subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type") - def test_do_not_add_without_an_annotation_context(self, ifc, collector, drawing): + def test_create_a_missing_annotation_context_on_the_fly(self, ifc, collector, drawing): drawing.get_drawing_target_view("drawing").should_be_called().will_return("target_view") drawing.get_annotation_context("target_view", "object_type").should_be_called().will_return(None) + drawing.create_annotation_context("target_view", "object_type").should_be_called().will_return("context") + drawing.show_decorations().should_be_called() + drawing.create_annotation_object("drawing", "object_type").should_be_called().will_return("obj") + ifc.get_entity("obj").should_be_called().will_return(None) + drawing.get_ifc_representation_class("object_type").should_be_called().will_return("ifc_representation_class") + drawing.run_root_assign_class( + obj="obj", + ifc_class="IfcAnnotation", + predefined_type="object_type", + should_add_representation=True, + context="context", + ifc_representation_class="ifc_representation_class", + ).should_be_called().will_return("element") + drawing.get_drawing_group("drawing").should_be_called().will_return("group") + ifc.run("group.assign_group", group="group", products=["element"]).should_be_called() + collector.assign("obj").should_be_called() + drawing.enable_editing("obj").should_be_called() subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type") diff --git a/src/blenderbim/test/files/BrickStub.ttl b/src/blenderbim/test/files/BrickStub.ttl index 883ec92353..bca967082d 100644 --- a/src/blenderbim/test/files/BrickStub.ttl +++ b/src/blenderbim/test/files/BrickStub.ttl @@ -22,3 +22,16 @@ brick:Location a owl:Class ; brick:Building a owl:Class ; rdfs:subClassOf brick:Class, brick:Location . + +brick:Chiller a owl:Class, + sh:NodeShape ; + rdfs:subClassOf brick:HVAC_Equipment . + +brick:HVAC_Equipment a owl:Class, + sh:NodeShape ; + rdfs:subClassOf brick:Equipment . + +brick:Equipment a owl:Class, + sh:NodeShape ; + rdfs:subClassOf brick:Class, + brick:Entity . diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 14687ce694..b70b9b2240 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -19,6 +19,8 @@ import os import bpy import brickschema +import brickschema.persistent +from brickschema.namespaces import REF, A import ifcopenshell import blenderbim.core.tool import blenderbim.tool as tool @@ -36,17 +38,17 @@ class TestImplementsTool(NewFile): class TestAddBrick(NewFile): def test_run(self): - BrickStore.graph = brickschema.Graph() - result = subject.add_brick("https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick( + "https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label" + ) assert "https://example.org/digitaltwin#" in result assert list( - BrickStore.graph.triples( - (URIRef(result), RDF.type, URIRef("https://brickschema.org/schema/Brick#Equipment")) - ) + BrickStore.graph.triples((URIRef(result), A, URIRef("https://brickschema.org/schema/Brick#Equipment"))) ) assert list( BrickStore.graph.triples( - (URIRef(result), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Unnamed")) + (URIRef(result), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("label")) ) ) @@ -59,6 +61,13 @@ class TestAddBrickBreadcrumb(NewFile): subject.add_brick_breadcrumb() assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[1].name == "brick_class" + def test_run_split_screen(self): + subject.set_active_brick_class("brick_class", split_screen=True) + subject.add_brick_breadcrumb(split_screen=True) + assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[0].name == "brick_class" + subject.add_brick_breadcrumb(split_screen=True) + assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[1].name == "brick_class" + class TestAddBrickFromElement(NewFile): def test_run(self): @@ -66,14 +75,14 @@ class TestAddBrickFromElement(NewFile): element = ifc.createIfcChiller() element.Name = "Chiller" element.GlobalId = ifcopenshell.guid.new() - BrickStore.graph = brickschema.Graph() + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") result = subject.add_brick_from_element( element, "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment" ) uri = f"http://example.org/digitaltwin#{element.GlobalId}" assert result == uri assert list( - BrickStore.graph.triples((URIRef(uri), RDF.type, URIRef("https://brickschema.org/schema/Brick#Equipment"))) + BrickStore.graph.triples((URIRef(uri), A, URIRef("https://brickschema.org/schema/Brick#Equipment"))) ) assert list( BrickStore.graph.triples( @@ -81,6 +90,25 @@ class TestAddBrickFromElement(NewFile): ) ) + def test_run_no_element_name(self): + ifc = ifcopenshell.file() + element = ifc.createIfcChiller() + element.GlobalId = ifcopenshell.guid.new() + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick_from_element( + element, "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment" + ) + uri = f"http://example.org/digitaltwin#{element.GlobalId}" + assert result == uri + assert list( + BrickStore.graph.triples((URIRef(uri), A, URIRef("https://brickschema.org/schema/Brick#Equipment"))) + ) + assert list( + BrickStore.graph.triples( + (URIRef(uri), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Unnamed")) + ) + ) + class TestAddBrickifcProject(NewFile): def test_run(self): @@ -88,66 +116,70 @@ class TestAddBrickifcProject(NewFile): tool.Ifc.set(ifc) project = ifc.createIfcProject(ifcopenshell.guid.new()) project.Name = "My Project" - BrickStore.graph = brickschema.Graph() + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") result = subject.add_brickifc_project("http://example.org/digitaltwin#") assert result == f"http://example.org/digitaltwin#{project.GlobalId}" brick = URIRef(result) + assert list(BrickStore.graph.triples((brick, A, REF.ifcProject))) + assert list(BrickStore.graph.triples((brick, REF.ifcProjectID, Literal(project.GlobalId)))) assert list( - BrickStore.graph.triples((brick, RDF.type, URIRef("https://brickschema.org/extension/ifc#Project"))) + BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file))) ) assert list( BrickStore.graph.triples( (brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("My Project")) ) ) - assert list( - BrickStore.graph.triples( - (brick, URIRef("https://brickschema.org/extension/ifc#projectID"), Literal(project.GlobalId)) - ) - ) - assert list( - BrickStore.graph.triples( - ( - brick, - URIRef("https://brickschema.org/extension/ifc#fileLocation"), - Literal(bpy.context.scene.BIMProperties.ifc_file), - ) - ) - ) class TestAddBrickifcReference(NewFile): def test_run(self): TestAddBrickifcProject().test_run() element = tool.Ifc.get().createIfcChiller(ifcopenshell.guid.new()) + element.Name = "Chiller" project = URIRef(f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}") subject.add_brickifc_reference("http://example.org/digitaltwin#foo", element, project) brick = URIRef("http://example.org/digitaltwin#foo") - bnode = list( - BrickStore.graph.triples((brick, URIRef("https://brickschema.org/extension/ifc#hasIFCReference"), None)) - )[0][2] - assert list( - BrickStore.graph.triples( - (bnode, URIRef("https://brickschema.org/extension/ifc#hasProjectReference"), project) - ) - ) - assert list( - BrickStore.graph.triples( - (bnode, URIRef("https://brickschema.org/extension/ifc#globalID"), Literal(element.GlobalId)) - ) - ) + bnode = list(BrickStore.graph.triples((brick, A, REF.IFCReference))) + assert list(BrickStore.graph.triples((bnode, REF.hasIfcProjectReference, URIRef(project)))) + assert list(BrickStore.graph.triples((bnode, REF.ifcGlobalID, Literal(element.GlobalId)))) + assert list(BrickStore.graph.triples((bnode, REF.ifcName, Literal(element.Name)))) -class TestAddFeed(NewFile): +class TestAddRelation(NewFile): def test_run(self): - BrickStore.graph = brickschema.Graph() - subject.add_feed("http://example.org/digitaltwin#source", "http://example.org/digitaltwin#destination") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + source = subject.add_brick( + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "source" + ) + destination = subject.add_brick( + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "destination" + ) + subject.add_relation(source, "https://brickschema.org/schema/Brick#feeds", destination) assert list( BrickStore.graph.triples( ( - URIRef("http://example.org/digitaltwin#source"), + URIRef(source), URIRef("https://brickschema.org/schema/Brick#feeds"), - URIRef("http://example.org/digitaltwin#destination"), + URIRef(destination), + ) + ) + ) + + +class TestRemoveRelation(NewFile): + def test_run(self): + TestAddRelation().test_run() + source, relation, destination = list( + BrickStore.graph.triples((None, URIRef("https://brickschema.org/schema/Brick#feeds"), None)) + )[0] + subject.remove_relation(source, relation, destination) + assert not list( + BrickStore.graph.triples( + ( + URIRef(source), + URIRef(relation), + URIRef(destination), ) ) ) @@ -159,30 +191,43 @@ class TestClearBrickBrowser(NewFile): subject.clear_brick_browser() assert len(bpy.context.scene.BIMBrickProperties.bricks) == 0 + def test_run_split_screen(self): + bpy.context.scene.BIMBrickProperties.split_screen_bricks.add() + subject.clear_brick_browser(split_screen=True) + assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 0 + class TestClearProject(NewFile): def test_run(self): BrickStore.graph = "graph" bpy.context.scene.BIMBrickProperties.active_brick_class == "brick_class" - bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.add().name = "foo" + bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "brick_class2" subject.clear_project() assert BrickStore.graph is None assert bpy.context.scene.BIMBrickProperties.active_brick_class == "" - assert len(bpy.context.scene.BIMBrickProperties.brick_breadcrumbs) == 0 + assert bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "" class TestExportBrickAttributes(NewFile): def test_run(self): - assert subject.export_brick_attributes("http://example.org/digitaltwin#floor") == { - "Identification": "http://example.org/digitaltwin#floor", - "Name": "floor", + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + brick = subject.add_brick( + "https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name" + ) + assert subject.export_brick_attributes(brick) == { + "Identification": brick, + "Name": "name", } def test_run_ifc2x3(self): + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + brick = subject.add_brick( + "https://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "name" + ) tool.Ifc.set(ifcopenshell.file(schema="IFC2X3")) - assert subject.export_brick_attributes("http://example.org/digitaltwin#floor") == { - "ItemReference": "http://example.org/digitaltwin#floor", - "Name": "floor", + assert subject.export_brick_attributes(brick) == { + "ItemReference": brick, + "Name": "name", } @@ -191,6 +236,10 @@ class TestGetActiveBrickClass(NewFile): subject.set_active_brick_class("brick_class") assert subject.get_active_brick_class() == "brick_class" + def test_run_split_screen(self): + subject.set_active_brick_class("brick_class", split_screen=True) + assert subject.get_active_brick_class(split_screen=True) == "brick_class" + class TestGetBrick(NewFile): def test_run(self): @@ -247,6 +296,51 @@ class TestGetConvertableBrickElements(NewFile): assert subject.get_convertable_brick_elements() == {element} +class TestGetConvertableBrickSpaces(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + element = ifc.createIfcBuildingStorey() + ifc.createIfcWall() + assert subject.get_convertable_brick_spaces() == {element} + + +class TestGetConvertableBrickSystems(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + element = ifc.createIfcSystem() + ifc.createIfcWall() + assert subject.get_convertable_brick_systems() == {element} + + +class TestGetParentSpace(NewFile): + def test_run(cls): + ifc = ifcopenshell.file() + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingStorey") + subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") + project = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", ifc, product=element, relating_object=project) + assert subject.get_parent_space(subelement) == element + assert subject.get_parent_space(element) is None + + +class TestGetElementContainer(NewFile): + def test_nothing(cls): + pass + + +class TestGetElementSystems(NewFile): + def test_nothing(cls): + pass + + +class TestGetElementFeeds(NewFile): + def test_run(cls): + pass + + class TestGetItemClass(NewFile): def test_run(self): TestLoadBrickFile().test_run() @@ -291,6 +385,21 @@ class TestImportBrickClasses(NewFile): assert brick.total_items == 1 assert not brick.label + def test_run_split_sccreen(self): + TestLoadBrickFile().test_run() + subject.import_brick_classes("Class", split_screen=True) + assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 2 + brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[0] + assert brick.name == "Building" + assert brick.uri == "https://brickschema.org/schema/Brick#Building" + assert brick.total_items == 1 + assert not brick.label + brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[1] + assert brick.name == "Location" + assert brick.uri == "https://brickschema.org/schema/Brick#Location" + assert brick.total_items == 1 + assert not brick.label + class TestImportBrickItems(NewFile): def test_run(self): @@ -303,6 +412,16 @@ class TestImportBrickItems(NewFile): assert brick.uri == "https://example.org/digitaltwin#bldg" assert brick.total_items == 0 + def test_run_split_screen(self): + TestLoadBrickFile().test_run() + subject.import_brick_items("Building", split_screen=True) + assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 1 + brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[0] + assert brick.name == "bldg" + assert brick.label == "My Building" + assert brick.uri == "https://example.org/digitaltwin#bldg" + assert brick.total_items == 0 + class TestLoadBrickFile(NewFile): def test_run(self): @@ -315,6 +434,8 @@ class TestLoadBrickFile(NewFile): filepath = os.path.join(cwd, "..", "files", "spaces.ttl") subject.load_brick_file(filepath) assert BrickStore.graph + namespaces = [(ns[0], ns[1].toPython()) for ns in BrickStore.graph.namespaces()] + assert ("brick", "https://brickschema.org/schema/Brick#") in namespaces class TestNewBrickFile(NewFile): @@ -340,14 +461,37 @@ class TestPopBrickBreadcrumb(NewFile): assert len(bpy.context.scene.BIMBrickProperties.brick_breadcrumbs) == 1 assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[0].name == "foo" + def test_run_split_screen(self): + bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.add().name = "foo" + bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.add().name = "bar" + assert subject.pop_brick_breadcrumb(split_screen=True) == "bar" + assert len(bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs) == 1 + assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[0].name == "foo" + class TestRemoveBrick(NewFile): def test_run(self): - BrickStore.graph = brickschema.Graph() - result = subject.add_brick("http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick( + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label" + ) subject.remove_brick(result) assert not list(BrickStore.graph.triples((URIRef(result), None, None))) + def test_run_with_bnode(self): + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + result = subject.add_brick( + "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment", "label" + ) + TestAddBrickifcProject().test_run() + element = tool.Ifc.get().createIfcChiller(ifcopenshell.guid.new()) + element.Name = "Chiller" + project = URIRef(f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}") + subject.add_brickifc_reference(result, element, project) + subject.remove_brick(result) + assert not list(BrickStore.graph.triples((URIRef(result), None, None))) + assert not list(BrickStore.graph.triples((None, REF.hasIfcProjectReference, None))) + class TestRunAssignBrickReference(NewFile): def test_nothing(self): @@ -359,7 +503,7 @@ class TestRunRefreshBrickViewer(NewFile): pass -class TestViewBrickClass(NewFile): +class TestRunViewBrickClass(NewFile): def test_nothing(self): pass @@ -369,6 +513,10 @@ class TestSelectBrowserItem(NewFile): subject.set_active_brick_class("brick_class") assert bpy.context.scene.BIMBrickProperties.active_brick_class == "brick_class" + def test_run(self): + subject.set_active_brick_class("brick_class", split_screen=True) + assert bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "brick_class" + class TestSetActiveBrickClass(NewFile): def test_run(self): @@ -376,3 +524,15 @@ class TestSetActiveBrickClass(NewFile): bpy.context.scene.BIMBrickProperties.bricks.add().name = "bar" subject.select_browser_item("namespace#bar") assert bpy.context.scene.BIMBrickProperties.active_brick_index == 1 + + +class TestSerializeBrick(NewFile): + def test_nothing(self): + pass + + +class TestAddNamespace(NewFile): + def test_run(self): + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + subject.add_namespace("digitaltwin", "http://example.org/digitaltwin") + assert ("digitaltwin", "http://example.org/digitaltwin") in BrickStore.namespaces diff --git a/src/blenderbim/test/tool/test_drawing.py b/src/blenderbim/test/tool/test_drawing.py index 86eecc143e..f7fe0c4743 100644 --- a/src/blenderbim/test/tool/test_drawing.py +++ b/src/blenderbim/test/tool/test_drawing.py @@ -376,7 +376,7 @@ class TestGenerateDrawingMatrix(NewFile): def test_creating_an_rcp_at_the_origin(self): assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", None) == mathutils.Matrix( - ((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1)) + ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1)) ) def test_creating_an_rcp_at_the_cursor_at_a_storey(self): @@ -389,7 +389,7 @@ class TestGenerateDrawingMatrix(NewFile): obj.matrix_world[2][3] = 3 bpy.context.scene.cursor.location = (1.0, 2.0, 0.0) assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", element.id()) == mathutils.Matrix( - ((-1, 0, 0, 1), (0, 1, 0, 2), (0, 0, -1, 3 + 1.6), (0, 0, 0, 1)) + ((1, 0, 0, 1), (0, 1, 0, 2), (0, 0, -1, 3 + 1.6), (0, 0, 0, 1)) ) def test_creating_a_north_elevation_at_the_cursor(self): @@ -485,9 +485,12 @@ class TestImportDrawings(NewFile): ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"TargetView": "PLAN_VIEW"}) subject.import_drawings() props = bpy.context.scene.DocProperties - assert props.drawings[0].ifc_definition_id == drawing.id() - assert props.drawings[0].name == "FOOBAR" - assert props.drawings[0].target_view == "PLAN_VIEW" + for d in props.drawings: + d.is_expanded = True + subject.import_drawings() + assert props.drawings[1].target_view == "PLAN_VIEW" + assert props.drawings[2].ifc_definition_id == drawing.id() + assert props.drawings[2].name == "FOOBAR" class TestImportSchedules(NewFile): @@ -668,6 +671,7 @@ class TestDrawingMaintainingSheetPosition(NewFile): return drawing_data def test_run(self): + props = bpy.context.scene.DocProperties bpy.ops.bim.create_project() ifc = tool.Ifc.get() sheet_path = Path.cwd() / "layouts" / "A00 - UNTITLED.svg" @@ -676,9 +680,14 @@ class TestDrawingMaintainingSheetPosition(NewFile): bpy.ops.bim.add_sheet() bpy.ops.bim.load_drawings() + for d in props.drawings: + d.is_expanded = True bpy.ops.bim.add_drawing() drawing = ifc.by_type("IfcAnnotation")[0] + for i, d in enumerate(props.drawings): + if d.ifc_definition_id == drawing.id(): + props.active_drawing_index = i bpy.ops.bim.activate_drawing(drawing=drawing.id()) bpy.ops.bim.create_drawing() bpy.ops.bim.add_drawing_to_sheet() diff --git a/src/blenderbim/test/tool/test_root.py b/src/blenderbim/test/tool/test_root.py index 5f9ac6c063..29ad103472 100644 --- a/src/blenderbim/test/tool/test_root.py +++ b/src/blenderbim/test/tool/test_root.py @@ -160,11 +160,12 @@ class TestSetObjectName(NewFile): obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) element = ifc.createIfcWall() subject.set_object_name(obj, element) - assert obj.name == "IfcWall/Object" + assert obj.name == "IfcWall/Unnamed" - def test_existing_ifc_prefixes_are_not_repeated(self): + def test_existing_blender_names_are_ignored(self): ifc = ifcopenshell.file() obj = bpy.data.objects.new("IfcSlab/Object", bpy.data.meshes.new("Mesh")) element = ifc.createIfcWall() + element.Name = "Foobar" subject.set_object_name(obj, element) - assert obj.name == "IfcWall/Object" + assert obj.name == "IfcWall/Foobar" diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index b99288ca4b..621f2d6595 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -115,7 +115,7 @@ class IfcCsv: self.group_results(groups, attributes) self.summarise_results(summaries, attributes) - self.sort_results(sort, attributes) + self.sort_results(sort, attributes, include_global_id) if format == "csv": self.export_csv(output, delimiter=delimiter) @@ -225,7 +225,7 @@ class IfcCsv: self.summaries[si] = max(summary_values[si]) self.summaries[si] = summary_type.title() + ": " + str(self.summaries[si]) - def sort_results(self, sort, attributes): + def sort_results(self, sort, attributes, include_global_id): if sort: def natural_sort(value): if isinstance(value, str): diff --git a/src/ifcopenshell-python/docs/ifcpatch.rst b/src/ifcopenshell-python/docs/ifcpatch.rst index 7a296e4853..2923e2227c 100644 --- a/src/ifcopenshell-python/docs/ifcpatch.rst +++ b/src/ifcopenshell-python/docs/ifcpatch.rst @@ -47,7 +47,7 @@ In this example, we'll extract out all `IfcWall` elements. :: - $ ifcpatch -i input.ifc -o output.ifc -r ExtractElements -a ".IfcWall" + $ ifcpatch -i input.ifc -o output.ifc -r ExtractElements -a "IfcWall" $ cat output.ifc Here is a minimal example of how to use IfcPatch as a library: @@ -60,7 +60,7 @@ Here is a minimal example of how to use IfcPatch as a library: "input": "input.ifc", "file": ifcopenshell.open("input.ifc"), "recipe": "ExtractElements", - "arguments": [".IfcWall"], + "arguments": ["IfcWall"], }) ifcpatch.write(output, "output.ifc") diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py index 8619b32acb..2941eb8308 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py @@ -58,4 +58,5 @@ class Usecase: if self.settings["objective"]: benchmark_values = list(self.settings["objective"].BenchmarkValues or []) benchmark_values.append(metric) + self.settings["objective"].BenchmarkValues = benchmark_values return metric diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py new file mode 100644 index 0000000000..072c71ecb0 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py @@ -0,0 +1,45 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell + +class Usecase: + def __init__(self, file, metric=None, reference_path=None): + """ + Adds a chain of references to a metric. The reference path is a string of the form "attribute.attribute.attribute" + Used to reference a value of an attribute of an instance through a metric objective entity. + """ + self.file = file + self.settings = {"metric": metric, "reference_path": reference_path} + + def execute(self): + if self.settings["reference_path"]: + attributes = self.settings["reference_path"].split(".") + references_created = [] + for i in range(len(attributes)): + if i == 0: + reference = self.file.create_entity("IfcReference") + reference.AttributeIdentifier = attributes[i] + self.settings["metric"].ReferencePath = reference + references_created.append(reference) + else: + reference = self.file.create_entity("IfcReference") + reference.AttributeIdentifier = attributes[i] + references_created[i-1].InnerReference = reference + references_created.append(reference) + return references_created \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py index e5dfa02dda..229f198920 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py @@ -43,6 +43,10 @@ class Usecase: self.settings = {"metric": metric} def execute(self): + if self.settings["metric"].ReferencePath: + reference = self.settings["metric"].ReferencePath + self.delete_reference(reference) + self.file.remove(self.settings["metric"]) for rel in self.file.by_type("IfcRelAssociatesConstraint"): if not rel.RelatingConstraint: @@ -50,3 +54,8 @@ class Usecase: for resource_rel in self.file.by_type("IfcResourceConstraintRelationship"): if not resource_rel.RelatingConstraint: self.file.remove(resource_rel) + + def delete_reference(self, reference): + if reference.InnerReference: + self.delete_reference(reference.InnerReference) + self.file.remove(reference) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py index 64fba5975c..7869a1c317 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py @@ -50,9 +50,7 @@ class Usecase: 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) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py new file mode 100644 index 0000000000..08602c9f0f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py @@ -0,0 +1,64 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import math +import ifcopenshell.api +import ifcopenshell.util.date +import ifcopenshell.util.element +import ifcopenshell.util.resource + + +class Usecase: + def __init__(self, file, resource=None): + """Calculates the number of resources required to perform scheduled work on a task. + """ + self.file = file + self.settings = {"resource": resource} + + def execute(self): + if ifcopenshell.util.constraint.is_attribute_locked(self.settings["resource"], "Usage.ScheduleUsage"): + return + if ( + not self.settings["resource"].Usage + or not self.settings["resource"].Usage.ScheduleWork + ): + return + + task = ifcopenshell.util.resource.get_task_assignments( + self.settings["resource"] + ) + if not task or not task.TaskTime: + return + + if not task.TaskTime.DurationType or task.TaskTime.DurationType == "WORKTIME": + hours_per_day = 8 + else: + hours_per_day = 24 + + task_duration = ifcopenshell.util.date.ifc2datetime( + task.TaskTime.ScheduleDuration + ) + seconds = task_duration.days * hours_per_day * 60 * 60 + seconds += task_duration.seconds + + person_hours = ifcopenshell.util.date.ifc2datetime( + self.settings["resource"].Usage.ScheduleWork + ) + + required_resources = person_hours.total_seconds() / seconds + self.settings["resource"].Usage.ScheduleUsage = float(required_resources) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py index c1d09b3ea0..a3df2a710b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py @@ -60,6 +60,8 @@ class Usecase: self.settings = {"resource": resource} def execute(self): + if ifcopenshell.util.constraint.is_attribute_locked(self.settings["resource"], "Usage.ScheduleWork"): + return amount_worked = ifcopenshell.util.resource.get_resource_required_work( self.settings["resource"] ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py index 451fb62658..6a4b2029d4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import datetime -import ifcopenshell.util.date +import ifcopenshell class Usecase: @@ -77,6 +77,11 @@ class Usecase: del self.settings["attributes"]["ActualFinish"] for name, value in self.settings["attributes"].items(): + metrics = ifcopenshell.util.constraint.get_metric_constraints( + self.resource, "Usage." + name + ) + if metrics and ifcopenshell.util.constraint.is_hard_constraint(metrics[0]): + continue if value: if "Start" in name or "Finish" in name or name == "StatusTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") @@ -87,6 +92,17 @@ class Usecase: ): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") setattr(self.settings["resource_time"], name, value) + if ( + name == "ScheduleUsage" + and ifcopenshell.util.constraint.get_metric_constraints( + self.resource, "Usage.ScheduleWork" + ) + ): + task = ifcopenshell.util.resource.get_task_assignments(self.resource) + if task: + ifcopenshell.api.run( + "sequence.calculate_task_duration", self.file, task=task + ) def get_resource(self): return [ diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py index 7ccb1056ce..4aed2fd050 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -125,6 +125,8 @@ class Usecase: or "ScheduleDuration" in self.settings["attributes"].keys() ): ifcopenshell.api.run("sequence.cascade_schedule", self.file, task=self.task) + if self.settings["task_time"].ScheduleDuration: + self.handle_resource_calculation() def calculate_finish(self): finish = ifcopenshell.util.sequence.get_start_or_finish_date( @@ -173,3 +175,12 @@ class Usecase: for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask") ][0] + + def handle_resource_calculation(self): + resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False) + for resource in resources: + if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"): + ifcopenshell.api.run("resource.calculate_resource_usage", self.file, resource=resource) + #TODO: If the duration changes, this implies the productivity rate must change to accomModate the new Schedule Work to be calculated. + # elif ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleUsage"): + # ifcopenshell.api.run("resource.calculate_resource_work", self.file, resource=resource) diff --git a/src/ifcopenshell-python/ifcopenshell/draw.py b/src/ifcopenshell-python/ifcopenshell/draw.py index df4169ddd8..ce2d6278df 100644 --- a/src/ifcopenshell-python/ifcopenshell/draw.py +++ b/src/ifcopenshell-python/ifcopenshell/draw.py @@ -61,6 +61,7 @@ class draw_settings: include_projection: bool = True prefilter: bool = True include_curves: bool = False + unify_inputs: bool = True def main(settings, files, iterators=None, merge_projection=True, progress_function=DO_NOTHING): @@ -135,6 +136,7 @@ def main(settings, files, iterators=None, merge_projection=True, progress_functi sr.setSubtractionSettings(W.ALWAYS) sr.setUsePrefiltering(settings.prefilter) + sr.setUnifyInputs(settings.unify_inputs) try: sh = ["none", "full", "left"].index(settings.storey_heights) diff --git a/src/ifcopenshell-python/ifcopenshell/util/constraint.py b/src/ifcopenshell-python/ifcopenshell/util/constraint.py new file mode 100644 index 0000000000..b2a38d6060 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/constraint.py @@ -0,0 +1,75 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult, Yassine Oualid +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . +# +# + +def get_constraints(product): + constraints = [] + for rel in product.HasAssociations or []: + if rel.is_a("IfcRelAssociatesConstraint"): + constraints.append(rel.RelatingConstraint) + return constraints + +def get_metrics(constraint): + metrics = [] + for metric in constraint.BenchmarkValues or []: + metrics.append(metric) + return metrics + +def get_metric_reference(metric, is_deep=True): + def get_reference_Attribute(ref, path): + if ref: + if is_deep: + if not path: + path = ref.AttributeIdentifier + else: + path += ".{}".format(ref.AttributeIdentifier) if ref.AttributeIdentifier else "" + return get_reference_Attribute(ref.InnerReference, path) + else: + return ref.AttributeIdentifier + return path + + reference = metric.ReferencePath + return get_reference_Attribute(reference, "") + +def get_metric_constraints(resource, attribute): + metrics = [] + for constraint in get_constraints(resource) or []: + for metric in get_metrics(constraint) or []: + if bool( + get_metric_reference(metric, is_deep=False) == attribute + or get_metric_reference(metric, is_deep=True) == attribute + ): + metrics.append(metric) + if metrics: + return metrics + return None + +def is_hard_constraint(metric): + if metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO": + return True + +def is_attribute_locked(product, attribute): + is_locked = False + metrics = get_metric_constraints( + product, attribute + ) + for metric in metrics or []: + if is_hard_constraint(metric): + is_locked = True + return is_locked diff --git a/src/ifcopenshell-python/ifcopenshell/util/date.py b/src/ifcopenshell-python/ifcopenshell/util/date.py index 8f7c45c054..27e6df4af0 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/date.py +++ b/src/ifcopenshell-python/ifcopenshell/util/date.py @@ -98,7 +98,7 @@ def readable_ifc_duration(string): final_string += f"{months} m " if months else "" final_string += f"{weeks} w " if weeks else "" final_string += f"{days} d " if days else "" - final_string += f"{hours} h " if hours else "" + final_string += f"{round(float(hours),2)} h " if hours else "" final_string += f"{minutes} m " if minutes else "" final_string += f"{seconds} s " if seconds else "" diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 80923b6c2a..ac84cc7046 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -88,6 +88,7 @@ def get_pset(element, name, prop=None, should_inherit=True): return type_pset return value + def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True): """Retrieve property sets, their related properties' names & values and ids. @@ -451,7 +452,6 @@ def get_styles(element): return styles - def get_elements_by_material(ifc_file, material): """Retrieves the elements related to a material. @@ -475,7 +475,7 @@ def get_elements_by_material(ifc_file, material): results = set() for inverse in ifc_file.get_inverse(material): if inverse.is_a("IfcRelAssociatesMaterial"): - results.update(inverse.RelatedObjects) + results.update(inverse.RelatedObjects or []) # See Revit bug #675 elif inverse.is_a("IfcMaterialLayer"): for material_set in inverse.ToMaterialLayerSet: results.update(get_elements_by_material(ifc_file, material_set)) @@ -869,7 +869,7 @@ def unbatch_remove_deep2(ifc_file): :rtype: ifcopenshell.file.file """ ifc_string = ifc_file.to_string() - lines = iter(ifc_string.split('\n')) + lines = iter(ifc_string.split("\n")) ids_to_delete = iter(sorted([e.id() for e in ifc_file.to_delete])) id_to_delete = next(ids_to_delete, None) result = [] diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py index 016a653209..f1e17dcc42 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/resource.py +++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py @@ -23,14 +23,18 @@ import ifcopenshell.util.date def get_productivity(resource, should_inherit=True): productivity = ifcopenshell.util.element.get_psets(resource).get("EPset_Productivity", None) if should_inherit and not productivity: - # Proposal for Schema - If instance doesn't have any productivity, inherit it's parent's productivity - if not resource.Nests: - return None - else: - parent_resource = resource.Nests[0].RelatingObject - productivity = ifcopenshell.util.element.get_psets(parent_resource).get("EPset_Productivity", None) + #Note: This is not part of the Schema - but it makes sense to inherit from parent + productivity = get_parent_productivity(resource) return productivity +def get_parent_productivity(resource): + if not resource.Nests: + return + else: + parent_resource = resource.Nests[0].RelatingObject + productivity = ifcopenshell.util.element.get_psets(parent_resource).get("EPset_Productivity", None) + return productivity + def get_unit_consumed(productivity): duration = productivity.get("BaseQuantityConsumed", None) @@ -80,6 +84,12 @@ def get_parametric_resource_products(resource): products.append(rel2.RelatingProduct) return products +def get_task_assignments(resource): + for rel in resource.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToProcess"): + continue + return rel.RelatingProcess + def get_resource_required_work(resource): productivity = get_productivity(resource) @@ -102,3 +112,6 @@ def get_resource_required_work(resource): required_work = total_quantity_to_produce * productivity_ratio iso_string = f"P{required_work}D" return iso_string + +def get_nested_resources(resource): + return [object for rel in resource.IsNestedBy or [] for object in rel.RelatedObjects] diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 1f62223b7b..097ea409ff 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -42,14 +42,14 @@ filter_elements_grammar = lark.Lark( location: "location" comparison value query: "query:" keys comparison value - pset: quoted_string | unquoted_string | regex_string - prop: quoted_string | unquoted_string | regex_string + pset: quoted_string | regex_string | unquoted_string + prop: quoted_string | regex_string | unquoted_string keys: quoted_string | unquoted_string attribute_name: /[A-Z]\\w+/ ifc_class: /Ifc\\w+/ - value: special | quoted_string | unquoted_string | regex_string + value: special | quoted_string | regex_string | unquoted_string unquoted_string: /[^.=\\s]+/ quoted_string: ESCAPED_STRING regex_string: "/" /[^\\/]+/ "/" @@ -119,7 +119,9 @@ def get_element_value(element, query): return Selector.get_element_value(element, filter_query["keys"], filter_query["is_regex"]) -def filter_elements(ifc_file, query, elements=None): +def filter_elements(ifc_file, query, elements=None, edit_in_place=False): + if elements and not edit_in_place: + elements = elements.copy() transformer = FacetTransformer(ifc_file, elements) transformer.transform(filter_elements_grammar.parse(query)) return transformer.get_results() @@ -426,7 +428,7 @@ class FacetTransformer(lark.Transformer): value = float(value) result = element_value == value elif isinstance(value, re.Pattern): - result = bool(value.match(element_value)) + result = bool(value.match(element_value)) if element_value is not None else False elif value in (None, True, False): result = element_value is value return result if comparison == "=" else not result diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 27db7330d5..02ec945a63 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -248,7 +248,7 @@ def get_task_work_schedule(task): def get_nested_tasks(task): - return [object for rel in task.IsNestedBy for object in rel.RelatedObjects] + return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects] def get_parent_task(task): diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 87a1b3652a..aef9bca169 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -928,131 +928,7 @@ class ShapeBuilder: start_offset = V(0, 0, start_length) end_extrusion_offset = start_offset.copy() - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file) - - # TODO: move to separate shape_builder method - # so we could check transition length without creating representation - def get_transition_length(start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True): - """get the final transition length for two profiles dimensions, angle and XY offset between them, - - the difference from `calculate_transition` - `get_transition_length` is making sure - that length will fit both sides of the transition - """ - print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None - - # offsets tend to have bunch of float point garbage - # that can result in errors when we're calculating value for square root below - offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion) - diff = start_half_dim.xy - end_half_dim.xy - diff = Vector([abs(i) for i in diff]) - - # TODO: move to separate shape_builder method - # so it could be tested later separately - def calculate_transition( - start_half_dim, end_half_dim, diff, offset, end_profile=False, angle=None, length=None - ): - """will return transition length based on the profile dimension differences and offset. - - If `length` is provided will return transition angle""" - - if end_profile: - diff, offset = diff.yx, offset.yx - - same_dimensions = is_x(diff.length, 0) - - a = diff.x + offset.x - b = diff.x - offset.x - if length is None: - if not same_dimensions: - if diff.x == 0: - return 0 - - t = tan(radians(angle)) - h = (a + b + sqrt(a**2 + 4 * a * b * t**2 + 2 * a * b + b**2)) / (2 * t) - length = sqrt(h**2 - offset.y**2) - - # TODO: move somewhere to tests? - if verbose: - A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0) - end_profile_offset = offset.to_3d() + V(0, 0, length) - D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0) - B, C = -A, -D - C += end_profile_offset - D += end_profile_offset - tested_angle = degrees((A - D).angle(B - C)) - print(f"II. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") - else: - if is_x(offset.x, 0): - angle = 90 # NOTE: for now we just hardcode the good value for that case - h = start_half_dim.x / tan(radians(angle / 2)) - length = sqrt(h**2 - offset.y**2) - - if verbose: # TODO: move to tests - O = V(0, 0, 0) - A = V(-start_half_dim.x, 0, length) + offset.to_3d() - B = A * V(-1, 1, 1) - tested_angle = degrees((A - O).angle(B - O)) - print(f"I. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") - else: - h = offset.x / tan(radians(angle)) - length = sqrt(h**2 - offset.y**2) - - if verbose: # TODO: move to tests - A = V(-start_half_dim.x, 0, 0) - H = A + V(0, 0, length) - D = H + offset.to_3d() - tested_angle = degrees((H - A).angle(D - A)) - print( - f"III. length = {length}, requested angle = {angle}, tested angle = {tested_angle}" - ) - - return length - - elif angle is None: - # TODO: write some tests here too - if not same_dimensions: - if length == 0: - return 0 - - h = sqrt(length**2 + offset.y**2) - t = -h * (a + b) / (a * b - h**2) - angle = degrees(atan(t)) - else: - h = sqrt(length**2 + offset.y**2) - if is_x(offset.x, 0): - angle = degrees(2 * atan(start_half_dim.x / h)) - else: - angle = degrees(atan(offset.x / length)) - return angle - - print(f"offset = {profile_offset} / {offset}") - print(f"diff = {diff}") - - calculation_arguments = (start_half_dim, end_half_dim, diff, offset) - - def check_transition(end_profile=False): - length = calculate_transition(*calculation_arguments, angle=angle, end_profile=end_profile) - other_side_angle = calculate_transition( - *calculation_arguments, length=length, end_profile=not end_profile - ) - - # NOTE: for now we just hardcode the good value for that case - same_dimensions = is_x(diff.length, 0) - if same_dimensions and is_x(offset.y if not end_profile else offset.x, 0): - requested_angle = 90 - else: - requested_angle = angle - - print(f"other_side_angle = {other_side_angle}, requested_angle = {requested_angle}") - # need to make sure that the worst angle (maximum angle) - # for this transition angle is `requested_angle` - if other_side_angle < requested_angle or is_x(other_side_angle, requested_angle): - print(f"final length = {length}, angle = {requested_angle}, other side angle = {other_side_angle}") - return length - - return check_transition() or check_transition(True) - - transition_length = get_transition_length(start_half_dim, end_half_dim, angle, profile_offset) + transition_length = self.mep_transition_length(start_half_dim, end_half_dim, angle, profile_offset) if transition_length is None: return None, None @@ -1199,3 +1075,154 @@ class ShapeBuilder: } return representation, transition_data + + # TODO: move to separate shape_builder method + # so we could check transition length without creating representation + def mep_transition_length(self, start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True): + """get the final transition length for two profiles dimensions, angle and XY offset between them, + + the difference from `calculate_transition` - `get_transition_length` is making sure + that length will fit both sides of the transition + """ + print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None + + # offsets tend to have bunch of float point garbage + # that can result in errors when we're calculating value for square root below + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file) + offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion) + diff = start_half_dim.xy - end_half_dim.xy + diff = Vector([abs(i) for i in diff]) + + print(f"offset = {profile_offset} / {offset}") + print(f"diff = {diff}") + + calculation_arguments = { + "start_half_dim": start_half_dim, + "end_half_dim": end_half_dim, + "diff": diff, + "offset": offset, + "verbose": verbose, + } + + def check_transition(end_profile=False): + length = self.mep_transition_calculate(**calculation_arguments, angle=angle, end_profile=end_profile) + if length is None: + return + + other_side_angle = self.mep_transition_calculate( + **calculation_arguments, length=length, end_profile=not end_profile + ) + + # NOTE: for now we just hardcode the good value for that case + same_dimension = is_x(diff.y if not end_profile else diff.x, 0) + if same_dimension and is_x(offset.y if not end_profile else offset.x, 0): + requested_angle = 90 + else: + requested_angle = angle + + print(f"other_side_angle = {other_side_angle}, requested_angle = {requested_angle}") + # need to make sure that the worst angle (maximum angle) + # for this transition angle is `requested_angle` + if other_side_angle < requested_angle or is_x(other_side_angle, requested_angle): + print(f"final length = {length}, angle = {requested_angle}, other side angle = {other_side_angle}") + return length + + return check_transition() or check_transition(True) + + def mep_transition_calculate( + self, start_half_dim, end_half_dim, offset, diff=None, end_profile=False, angle=None, length=None, verbose=True + ): + """will return transition length based on the profile dimension differences and offset. + + If `length` is provided will return transition angle""" + + print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None + + if diff is None: + diff = start_half_dim.xy - end_half_dim.xy + diff = Vector([abs(i) for i in diff]) + + if end_profile: + diff, offset = diff.yx, offset.yx + + same_dimension = is_x(diff.x, 0) + a = diff.x + offset.x + b = diff.x - offset.x + if length is None: + if not same_dimension: + t = tan(radians(angle)) + h0 = a**2 + 4 * a * b * t**2 + 2 * a * b + b**2 + # TODO: we might need to specify the exact failing cases in the future + if h0 < 0: + print( + f"B. Coulndn't calculate transition length for angle = {angle}, offset = {offset}, diff = {diff}" + ) + return None + + h = (a + b + sqrt(h0)) / (2 * t) + length_squared = h**2 - offset.y**2 + if length_squared <= 0: + print(f"B. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") + return None + length = sqrt(length_squared) + + if verbose: + A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0) + end_profile_offset = offset.to_3d() + V(0, 0, length) + D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0) + B, C = -A, -D + C += end_profile_offset + D += end_profile_offset + tested_angle = degrees((A - D).angle(B - C)) + print(f"A. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") + else: + if is_x(offset.x, 0): + angle = 90 # NOTE: for now we just hardcode the good value for that case + h = start_half_dim.x / tan(radians(angle / 2)) + length_squared = h**2 - offset.y**2 + if length_squared <= 0: + print(f"B. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") + return None + length = sqrt(length_squared) + + if verbose: + O = V(0, 0, 0) + A = V(-start_half_dim.x, 0, length) + offset.to_3d() + B = A * V(-1, 1, 1) + tested_angle = degrees((A - O).angle(B - O)) + print(f"B. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") + else: + h = offset.x / tan(radians(angle)) + length_squared = h**2 - offset.y**2 + if length_squared <= 0: + print(f"C. angle = {angle} requires h = {h} which is not possible with y offset = {offset.y}") + return None + length = sqrt(length_squared) + + if verbose: + A = V(-start_half_dim.x, 0, 0) + H = A + V(0, 0, length) + H.y += offset.y + D = H.copy() + D.x += offset.x + tested_angle = degrees((H - A).angle(D - A)) + print(f"C. length = {length}, requested angle = {angle}, tested angle = {tested_angle}") + + return length + + elif angle is None: + if not same_dimension: + if length == 0: + return 0 + + h = sqrt(length**2 + offset.y**2) + t = -h * (a + b) / (a * b - h**2) + angle = degrees(atan(t)) + + else: + h = sqrt(length**2 + offset.y**2) + if is_x(offset.x, 0): + angle = degrees(2 * atan(start_half_dim.x / h)) + else: + angle = degrees(atan(offset.x / h)) + return angle diff --git a/src/ifcopenshell-python/test/api/document/test_remove_information.py b/src/ifcopenshell-python/test/api/document/test_remove_information.py index d03a7341ad..ec2adaf9fb 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_information.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_information.py @@ -54,7 +54,6 @@ class TestRemoveInformation(test.bootstrap.IFC4): project = self.file.createIfcProject() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) information2 = ifcopenshell.api.run("document.add_information", self.file, parent=information) - ifcopenshell.api.run("document.add_reference", self.file, information=information) ifcopenshell.api.run("document.add_reference", self.file, information=information2) ifcopenshell.api.run("document.remove_information", self.file, information=information) assert len(self.file.by_type("IfcDocumentInformation")) == 0 diff --git a/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py b/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py index 040a9ba53f..83a9d9303d 100644 --- a/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py +++ b/src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.constraint class TestCalculateResourceWork(test.bootstrap.IFC4): diff --git a/src/ifcopenshell-python/test/util/test_brick.py b/src/ifcopenshell-python/test/util/test_brick.py index cefd7a80d4..52a99b1a1a 100644 --- a/src/ifcopenshell-python/test/util/test_brick.py +++ b/src/ifcopenshell-python/test/util/test_brick.py @@ -38,10 +38,3 @@ class TestGetBrickTypeIFC2X3(test.bootstrap.IFC2X3): type_element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcAirTerminalBoxType") ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type_element) assert subject.get_brick_type(element) == "https://brickschema.org/schema/Brick#TerminalUnit" - - -class TestGetBrickElementsIFC4(test.bootstrap.IFC4): - def test_run(self): - element = self.file.createIfcAirTerminalBox() - self.file.createIfcWall() - assert subject.get_brick_elements(self.file) == {element} diff --git a/src/ifcopenshell-python/test/util/test_pset.py b/src/ifcopenshell-python/test/util/test_pset.py index 9fefd61396..a953193b97 100644 --- a/src/ifcopenshell-python/test/util/test_pset.py +++ b/src/ifcopenshell-python/test/util/test_pset.py @@ -46,5 +46,6 @@ class TestPsetQto: names = self.pset_qto.get_applicable_names("IfcWall") assert "Pset_WallCommon" in names names = self.pset_qto.get_applicable_names("IfcWallType") - assert len(names) == 5 + assert len(names) == 6 assert "Pset_WallCommon" in names + assert "Qto_WallBaseQuantities" in names # Backported fix for IFC4 diff --git a/src/ifcopenshell-python/test/util/test_shape_builder.py b/src/ifcopenshell-python/test/util/test_shape_builder.py new file mode 100644 index 0000000000..c3b1b29fc5 --- /dev/null +++ b/src/ifcopenshell-python/test/util/test_shape_builder.py @@ -0,0 +1,164 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult , @Andrej730 +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +from ifcopenshell.util.shape_builder import ShapeBuilder, V, is_x +from math import degrees, radians, tan +from mathutils import Vector + + +class TestCalculateTransitions(test.bootstrap.IFC4): + def calculate_and_test(self, params, length): + end_profile = params["end_profile"] + start_half_dim = params["start_half_dim"] + end_half_dim = params["end_half_dim"] + offset = params["offset"] + offset = offset if not end_profile else offset.yx + angle = params["angle"] + + calculated_length = self.builder.mep_transition_calculate(**params) + if length is None: + assert calculated_length is None + return + + assert is_x(calculated_length, length) + + # angle confirmation methods: + # A - between two profiles of different dimensions + # B - between two profiles of same dimensions, no offset by x + # C - between two profiles of same dimensions, has offset by x + diff = start_half_dim.xy - end_half_dim.xy + same_dimension = is_x(diff.x if not end_profile else diff.y, 0) + if not same_dimension: + confirmation_method = "A" + else: + confirmation_method = "B" if is_x(offset.x, 0) else "C" + + if confirmation_method == "A": + A = (end_half_dim if end_profile else start_half_dim) * V(1, 0, 0) + end_profile_offset = offset.to_3d() + V(0, 0, length) + D = (start_half_dim if end_profile else end_half_dim) * V(1, 0, 0) + B, C = -A, -D + C += end_profile_offset + D += end_profile_offset + tested_angle = degrees((A - D).angle(B - C)) + assert is_x(tested_angle, angle) + + elif confirmation_method == "B": + O = V(0, 0, 0) + A = V(-start_half_dim.x, 0, length) + offset.to_3d() + B = A * V(-1, 1, 1) + tested_angle = degrees((A - O).angle(B - O)) + assert is_x(tested_angle, angle) + + elif confirmation_method == "C": + A = V(-start_half_dim.x, 0, 0) + H = A + V(0, 0, length) + H.y += offset.y + D = H.copy() + D.x += offset.x + tested_angle = degrees((H - A).angle(D - A)) + assert is_x(tested_angle, angle) + + angle = self.builder.mep_transition_calculate(**params | {"angle": None, "length": calculated_length}) + assert is_x(angle, angle) + + def test_mep_transition_same_dims_no_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(100, 50, 0), + "offset": V(0, 0), + "end_profile": False, + "angle": 90, + "verbose": True, + } + self.calculate_and_test(params, 100) + + def test_mep_transition_same_dims_has_x_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(100, 50, 0), + "offset": V(50, 50), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, 70.71068) + + def test_mep_transition_same_dims_has_y_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(100, 50, 0), + "offset": V(0, 50), + "end_profile": False, + "angle": 90, + "verbose": True, + } + self.calculate_and_test(params, 86.60254) + + def test_mep_transition_diff_dims_no_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(50, 100, 0), + "offset": V(0, 0), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, 186.60254) + + def test_mep_transition_diff_dims_has_x_y_offset(self): + self.builder = ShapeBuilder(self.file) + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(50, 100, 0), + "offset": V(50, 50), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, 165.83124) + + def test_mep_transition_y_offset_too_big(self): + self.builder = ShapeBuilder(self.file) + + # method A + params = { + "start_half_dim": V(100, 50, 0), + "end_half_dim": V(50, 100, 0), + # offset.y > h - 190 > 186.6 + "offset": V(0, 190), + "end_profile": False, + "angle": 30, + "verbose": True, + } + self.calculate_and_test(params, None) + + # method B + params["end_half_dim"] = V(100, 100, 0) + self.calculate_and_test(params, None) + + # method C + params["offset"].x = 10 + self.calculate_and_test(params, None) diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py index f21d2fd31d..fe1ced106e 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py @@ -37,13 +37,13 @@ class Patcher: .. code:: python # Extract all walls - ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcWall"]}) + ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["IfcWall"]}) # Extract all slabs - ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcSlab"]}) + ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["IfcSlab"]}) # Extract all walls and slabs - ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcWall|.IfcSlab"]}) + ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]}) """ self.src = src self.file = file @@ -59,8 +59,7 @@ class Patcher: self.owner_history = self.new.add(owner_history) break self.add_element(self.file.by_type("IfcProject")[0]) - selector = ifcopenshell.util.selector.Selector() - for element in selector.parse(self.file, self.query): + for element in ifcopenshell.util.selector.filter_elements(self.file, self.query): self.add_element(element) self.create_spatial_tree() self.file = self.new diff --git a/src/ifcsverchok/README.md b/src/ifcsverchok/README.md index 3f3ad6d44d..cdc605f17b 100644 --- a/src/ifcsverchok/README.md +++ b/src/ifcsverchok/README.md @@ -5,7 +5,7 @@ N.B.! IfcSverchok nodes are WIP. You can experience Blender crashes while using ## Packaged installation[](https://blenderbim.org/docs-python/ifcsverchok/installation.html#packaged-installation "Permalink to this headline") -IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://blenderbim.org/builds/ifcsverchok-230704.zip). +IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://blenderbim.org/builds/ifcsverchok-230823.zip). Like all Blender add-ons, they can be installed using `Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox`. You can enable add-ons permanently by using `Save User Settings` from the Addons menu. diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index f1962e6570..b11db7287c 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -699,6 +699,11 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { IfcUtil::IfcBaseEntity* storey = p ? p->first : nullptr; double elev = p ? p->second : std::numeric_limits::quiet_NaN(); // @todo is it correct to call nameElement() here with a single storey (what if this element spans multiple?) + + if (unify_inputs_) { + compound_local = IfcGeom::util::unify(compound_local, 1.e-6); + } + geometry_data data{ compound_local, dash_arrays, trsf, brep_obj->product(), storey, elev, brep_obj->name(), nameElement(storey, brep_obj) }; if (auto_section_ || auto_elevation_ || section_ref_ || elevation_ref_ || elevation_ref_guid_ || deferred_section_data_) { @@ -1187,14 +1192,7 @@ void SvgSerializer::write(const geometry_data& data) { // Iterate over components of compound to have better chance of matching section edges to closed wires for (; it.More(); it.Next(), ++dash_it) { - const TopoDS_Shape& subshape_before_unification = it.Value(); - TopoDS_Shape subshape; - - if (unify_inputs_) { - subshape = IfcGeom::util::unify(subshape_before_unification, 1. - 6); - } else { - subshape = subshape_before_unification; - } + const TopoDS_Shape& subshape = it.Value(); Bnd_Box bb; try {