From 706aa708991a0f402aabe88ba555e7664a30c0b0 Mon Sep 17 00:00:00 2001 From: arun Date: Tue, 8 Aug 2023 08:36:36 +0530 Subject: [PATCH 01/74] Update create_2pt_wall.py (#3553) * Update create_2pt_wall.py included unit scale for use cases other than si unit * Revised Update create_2pt_wall.py included is_si kwarg to select between si units and other units and also changed the dtype of p1 and p2 array to float incase they are entered as integers --- .../api/geometry/create_2pt_wall.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py index 983d6fb9c0..e228730974 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py @@ -22,7 +22,7 @@ import ifcopenshell.util.unit class Usecase: - def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None): + def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None, is_si=True): self.file = file self.settings = { "element": element, @@ -32,15 +32,25 @@ class Usecase: "elevation": elevation, "height": height, "thickness": thickness, + "is_si": is_si } def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - self.settings["p1"] = np.array(self.settings["p1"]) - self.settings["p2"] = np.array(self.settings["p2"]) + self.settings["p1"] = np.array(self.settings["p1"]).astype(float) + self.settings["p2"] = np.array(self.settings["p2"]).astype(float) length = float(np.linalg.norm(self.settings["p2"] - self.settings["p1"])) + + if not self.settings["is_si"]: + length=self.convert_unit_to_si(length) + self.settings["height"]=self.convert_unit_to_si(self.settings["height"]) + self.settings["thickness"]=self.convert_unit_to_si(self.settings["thickness"]) + self.settings["p1"][0] = self.convert_unit_to_si(self.settings["p1"][0]) + self.settings["p1"][1] = self.convert_unit_to_si(self.settings["p1"][1]) + self.settings["elevation"] = self.convert_unit_to_si(self.settings["elevation"]) + representation = ifcopenshell.api.run( "geometry.add_wall_representation", self.file, @@ -55,7 +65,7 @@ class Usecase: [ [v[0], -v[1], 0, self.settings["p1"][0]], [v[1], v[0], 0, self.settings["p1"][1]], - [0, 0, 1, self.convert_si_to_unit(self.settings["elevation"])], + [0, 0, 1, self.settings["elevation"]], [0, 0, 0, 1], ] ) @@ -64,7 +74,5 @@ class Usecase: ) return representation - def convert_si_to_unit(self, co): - if isinstance(co, (tuple, list)): - return [self.convert_si_to_unit(o) for o in co] - return co / self.settings["unit_scale"] + def convert_unit_to_si(self, co): + return co * self.settings["unit_scale"] From e43347161ae5db8d7474dbe6747945f271f197f0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 8 Aug 2023 22:35:27 +1000 Subject: [PATCH 02/74] Fix #3547. Bug where using existing clippings were not preserved when editing profile or wall representations. --- .../api/geometry/add_profile_representation.py | 8 ++++++-- .../ifcopenshell/api/geometry/add_wall_representation.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index d2eddcdeb0..fe028bc657 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -63,7 +63,11 @@ class Usecase: def apply_clippings(self, first_operand): while self.settings["clippings"]: clipping = self.settings["clippings"].pop() - if clipping["operand_type"] == "IfcHalfSpaceSolid": + if isinstance(clipping, ifcopenshell.entity_instance): + new = ifcopenshell.util.element.copy(self.file, clipping) + new.FirstOperand = first_operand + first_operand = new + elif clipping["operand_type"] == "IfcHalfSpaceSolid": matrix = clipping["matrix"] second_operand = self.file.createIfcHalfSpaceSolid( self.file.createIfcPlane( @@ -81,7 +85,7 @@ class Usecase: ), False, ) - first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) + first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) return first_operand def convert_si_to_unit(self, co): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py index c146c8cdf3..799e1b252f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py @@ -95,7 +95,11 @@ class Usecase: def apply_clippings(self, first_operand): while self.settings["clippings"]: clipping = self.settings["clippings"].pop() - if clipping["operand_type"] == "IfcHalfSpaceSolid": + if isinstance(clipping, ifcopenshell.entity_instance): + new = ifcopenshell.util.element.copy(self.file, clipping) + new.FirstOperand = first_operand + first_operand = new + elif clipping["operand_type"] == "IfcHalfSpaceSolid": matrix = clipping["matrix"] second_operand = self.file.createIfcHalfSpaceSolid( self.file.createIfcPlane( @@ -113,7 +117,7 @@ class Usecase: ), False, ) - first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) + first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand) return first_operand def convert_si_to_unit(self, co): From fea0d9316225e1f870d6d2b2f111cae3226b04b5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 8 Aug 2023 23:47:57 +1000 Subject: [PATCH 03/74] Fix SurfaceColour typo that broke loading colours in some IFCs --- src/blenderbim/blenderbim/tool/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py index 1b3ca755cc..5604f3f896 100644 --- a/src/blenderbim/blenderbim/tool/loader.py +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -110,7 +110,7 @@ class Loader(blenderbim.core.tool.Loader): "IfcNormalisedRatioMeasure" ): diffuse_color_value = surface_style["DiffuseColour"].wrappedValue - diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColor"][:3]] + [1] + diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColour"][:3]] + [1] surface_style["DiffuseColour"] = ("IfcNormalisedRatioMeasure", diffuse_color) else: surface_style["DiffuseColour"] = None From 0f68679295ad634aade30f5033c927481e3a6e14 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 9 Aug 2023 00:13:16 +1000 Subject: [PATCH 04/74] Fix #2753. Disable mapping representations when appending assets via the ExtractElements recipe to be more forgiving (and faster) on invalid models. --- .../ifcopenshell/api/project/append_asset.py | 1 + .../ifcopenshell/api/type/assign_type.py | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index f825445428..aeb880550b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -172,6 +172,7 @@ class Usecase: should_run_listeners=False, related_object=element, relating_type=new_type, + should_map_representations=False, ) ifcopenshell.api.owner.settings.restore() diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py index 33bc25ab72..324e6c548d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py @@ -22,7 +22,7 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, related_object=None, relating_type=None): + def __init__(self, file, related_object=None, relating_type=None, should_map_representations=True): """Assigns a type to an occurrence of an object IFC supports the concept of occurrences and types. An occurrence is an @@ -87,6 +87,11 @@ class Usecase: :type related_object: ifcopenshell.entity_instance.entity_instance :param relating_type: The IfcElementType type. :type relating_type: ifcopenshell.entity_instance.entity_instance + :param should_map_representations: If a type has a representation map, + IFC requires all occurrences to map those representations. Some IFC + vendors might disobey this, or you might want to handle it + yourself. In this scenario, you may set this to False. + :type should_map_representations: bool :return: The IfcRelDefinesByType relationship :rtype: ifcopenshell.entity_instance.entity_instance @@ -164,6 +169,7 @@ class Usecase: self.settings = { "related_object": related_object, "relating_type": relating_type, + "should_map_representations": should_map_representations, } def execute(self): @@ -207,14 +213,15 @@ class Usecase: } ) - if getattr(self.settings["relating_type"], "RepresentationMaps", None): - ifcopenshell.api.run( - "type.map_type_representations", - self.file, - related_object=self.settings["related_object"], - relating_type=self.settings["relating_type"], - ) - self.map_material_usages() + if self.settings["should_map_representations"]: + if getattr(self.settings["relating_type"], "RepresentationMaps", None): + ifcopenshell.api.run( + "type.map_type_representations", + self.file, + related_object=self.settings["related_object"], + relating_type=self.settings["relating_type"], + ) + self.map_material_usages() return types def map_material_usages(self): From c5e5036fd3a4681333043066db29f402ea2cacfa Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 9 Aug 2023 15:00:58 +1000 Subject: [PATCH 05/74] Store applicability in IfcTester JSON results --- src/ifctester/ifctester/reporter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index 0cac09d14d..c3c5bd7abc 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -163,6 +163,7 @@ class Json(Reporter): return self.results def report_specification(self, specification): + applicability = [a.to_string("applicability") for a in specification.applicability] requirements = [] for requirement in specification.requirements: requirements.append( @@ -182,6 +183,7 @@ class Json(Reporter): "total": total, "percentage": percentage, "required": specification.minOccurs != 0, + "applicability": applicability, "requirements": requirements, } From 9fc32e5c2ef8f610e605191404cb3fe0b93635c3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 9 Aug 2023 16:42:03 +1000 Subject: [PATCH 06/74] IfcTester JSON reporter now also stores identification data separately for convenient reporting. --- src/ifctester/ifctester/reporter.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index c3c5bd7abc..ee9e952269 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -21,6 +21,8 @@ import sys import math import logging import datetime +import ifcopenshell +import ifcopenshell.util.element cwd = os.path.dirname(os.path.realpath(__file__)) @@ -189,7 +191,17 @@ class Json(Reporter): def report_failed_entities(self, requirement): return [ - {"reason": requirement.failed_reasons[i], "element": str(e)} + { + "reason": requirement.failed_reasons[i], + "element": str(e), + "class": e.is_a(), + "predefined_type": ifcopenshell.util.element.get_predefined_type(e), + "name": getattr(e, "Name", None), + "description": getattr(e, "Description", None), + "id": e.id(), + "global_id": getattr(e, "GlobalId", None), + "tag": getattr(e, "Tag", None), + } for i, e in enumerate(requirement.failed_entities) ] From b6111510396f796fb18daef82286dc9f08a0d7d1 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 9 Aug 2023 14:55:23 +0800 Subject: [PATCH 07/74] #3536 fix edge segments in world coords --- src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp index e606304b63..7672eb381f 100644 --- a/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp +++ b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp @@ -428,6 +428,8 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model) for (int i = 1; i <= n; ++i) { gp_XYZ p = tessellater.Value(i).XYZ(); + auto p_local = p; + trsf.Transforms(p); int current = addVertex(iit->ItemId(), surface_style_id, p); @@ -455,11 +457,10 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model) } d3 = d1.XYZ() + d2.XYZ(); d4 = d1.XYZ() - d2.XYZ(); - p2 = p - d3.XYZ() / 10.; - p3 = p - d4.XYZ() / 10.; + p2 = p_local - d3.XYZ() / 10.; + p3 = p_local - d4.XYZ() / 10.; trsf.Transforms(p2); trsf.Transforms(p3); - trsf.Transforms(p); int left = addVertex(iit->ItemId(), surface_style_id, p2); int right = addVertex(iit->ItemId(), surface_style_id, p3); From 4dc55e844ce38622af23cd772de78dc8cdbd00a2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 4 Aug 2023 14:46:00 +0500 Subject: [PATCH 08/74] Fix default mep profile names Previously it was generating names like 4000000x4000000 in mm projects. --- .../blenderbim/bim/module/type/operator.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index 151eaf0dd0..ea07090640 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -312,32 +312,36 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): ) else: # NOTE: defaults dims are in meters / mm + # for now default names are hardcoded to mm if template == "FLOW_SEGMENT_RECTANGULAR": - default_x_dim = 0.4 / unit_scale - default_y_dim = 0.2 / unit_scale - profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_x_dim*1000}" + default_x_dim = 0.4 + default_y_dim = 0.2 + profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_y_dim*1000}" profile = ifc_file.create_entity( "IfcRectangleProfileDef", ProfileName=profile_name, ProfileType="AREA", - XDim=default_x_dim, - YDim=default_y_dim, + XDim=default_x_dim / unit_scale, + YDim=default_y_dim / unit_scale, ) elif template == "FLOW_SEGMENT_CIRCULAR": - default_diameter = 0.1 / unit_scale + default_diameter = 0.1 profile_name = f"{ifc_class}-{default_diameter*1000}" profile = ifc_file.create_entity( - "IfcCircleProfileDef", ProfileName=profile_name, ProfileType="AREA", Radius=default_diameter / 2 + "IfcCircleProfileDef", + ProfileName=profile_name, + ProfileType="AREA", + Radius=(default_diameter / 2) / unit_scale, ) elif template == "FLOW_SEGMENT_CIRCULAR_HOLLOW": - default_diameter = 0.15 / unit_scale - default_thickness = 0.005 / unit_scale + default_diameter = 0.15 + default_thickness = 0.005 profile_name = f"{ifc_class}-{default_diameter*1000}x{default_thickness*1000}" profile = ifc_file.create_entity( "IfcCircleHollowProfileDef", ProfileName=profile_name, ProfileType="AREA", - Radius=default_diameter / 2, + Radius=(default_diameter / 2) / unit_scale, WallThickness=default_thickness, ) From 2c1c13f27ede3a336e77cfb05c93d3b07aac35dd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 4 Aug 2023 14:46:48 +0500 Subject: [PATCH 09/74] bim.add_transition Added simple operator to add transition between two mep segments (now only rectangular collinear segments are supported). It also reuses the transition type that was previously used to connect segments of the same type. Demonstration - https://imgur.com/a/c1AOxj1 --- .../blenderbim/bim/module/model/__init__.py | 1 + .../blenderbim/bim/module/model/mep.py | 314 +++++++++++++++--- src/blenderbim/blenderbim/tool/cad.py | 50 ++- src/blenderbim/blenderbim/tool/system.py | 34 ++ .../ifcopenshell/util/shape_builder.py | 131 +++++++- 5 files changed, 471 insertions(+), 59 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 9d732a9141..ee5224b157 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -177,6 +177,7 @@ classes = ( roof.RemoveRoof, roof.SetGableRoofEdgeAngle, mep.MEPAddObstruction, + mep.MEPAddTransition, ) addon_keymaps = [] diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index ccbde59675..bcc45920c3 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -18,7 +18,10 @@ import bpy import math +import collections import bmesh +import re +import json import ifcopenshell import ifcopenshell.api import ifcopenshell.util.unit @@ -31,13 +34,13 @@ import blenderbim.core.type import blenderbim.core.root import blenderbim.core.geometry import blenderbim.tool as tool -from math import pi, degrees +from math import pi, degrees, radians +from copy import copy from mathutils import Vector, Matrix -import re +from ifcopenshell.util.shape_builder import ShapeBuilder from blenderbim.bim.module.model.profile import DumbProfileJoiner V = lambda *x: Vector([float(i) for i in x]) -float_is_zero = lambda f: 0.0001 >= f >= -0.0001 class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator): @@ -86,7 +89,7 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator): def process_branch(branch): for branch_element in branch: element = branch_element["element"] - print('processing', element) + print("processing", element) predecessor = branch_element["predecessor"] if False: # If the element does not need to be transformed, return early. return @@ -107,6 +110,9 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): + # TODO: need to add ui for parameters: + # - obstruction cap thickness + # - start/end thickness and angle for transition selected_objs = [] selected_profiles = [] @@ -207,12 +213,7 @@ class MEPGenerator: ports = tool.System.get_ports(segment) if segment.is_a("IfcFlowSegment") and not ports: - for mat in [start_port_matrix, end_port_matrix]: - # TODO: specify PredefinedType based on the segment type - port = tool.Ifc.run("system.add_port", element=segment) - port.FlowDirection = "NOTDEFINED" - port.PredefinedType = self.get_port_predefined_type(segment) - tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=mat, is_si=True) + tool.System.add_ports(obj) return # adjust current segment ports and related flow segments @@ -248,7 +249,6 @@ class MEPGenerator: ): if port_position == "start_port": if segment.is_a("IfcFlowFitting"): - profile_joiner = DumbProfileJoiner() connected_element_length = ( tool.Model.get_flow_segment_axis(connected_obj)[0] - tool.Model.get_flow_segment_axis(obj)[0] @@ -269,45 +269,103 @@ class MEPGenerator: extrusion_depth = segment_object.dimensions.z end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth) segment_data = { - "start_point": start_point, - "end_point": end_point, + "start_point": start_point.copy().freeze(), + "end_point": end_point.freeze(), "ports": ports, "extrusion_depth": extrusion_depth, } for port in ports: port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates) - if float_is_zero(port_local_position.length): + if tool.Cad.is_x(port_local_position.length, 0.0): segment_data["start_port"] = port else: segment_data["end_port"] = port return segment_data - def get_port_predefined_type(self, segment): - split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) - class_name = "".join(split_camel_case(segment.is_a())[1:-1]).upper() - if class_name == "CONVEYOR": - return "NOTDEFINED" - return class_name - def get_mep_element_class_name(self, element, mep_class_type): split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type]) return class_name - def get_compatible_fitting_type(self, segment, predefined_type): - """We find compatible fitting only by checking if they were - already used with that segment type before. + def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type): + """ + returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting. + + We find compatible fitting only by checking + if they were already used with that segment type before + and fitting's ports should match `port_or_ports` by PredefinedType and SystemType. + + If port from `port_or_ports` has PredefinedType/SystemType == None/NOTDEFINED then + those parameters won't be taken into account checking compatibility. There lies the problem that it won't be - able to identify the fittings that were not connected to any segments yet. + able to identify the fittings that were not yet connected to any segments yet. """ - segment_type = ifcopenshell.util.element.get_type(segment) - if not segment_type: - return None + if not isinstance(segment_or_segments, collections.abc.Iterable): + segments = [segment_or_segments] + ports = [port_or_ports] + else: + segments = segment_or_segments + ports = port_or_ports - fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segment, "Fitting")) + segments_data = [] + for segment, port in zip(segments, ports, strict=True): + segment_type = ifcopenshell.util.element.get_type(segment) + # if segment doesn't have type we cannot check compatibility by available occurences + if segment_type is None: + return + segments_data.append((segment_type, port.PredefinedType, port.SystemType)) + + def are_connected_elements_compatible(segments_data, fitting_data): + # prevent arguments mutation, not using deepcopy because of the errors with ifc elements + segments_data = [copy(i) for i in segments_data] + fitting_data = [copy(i) for i in fitting_data] + not_defined_values = {"NOTDEFINED", None} + + if len(segments_data) != len(fitting_data): + return False + + def are_segments_compatible(test_segment_data, base_segment_data): + segment_type, predefined_type, system_type = test_segment_data + base_segment_type, base_predefined_type, base_system_type = base_segment_data + + if segment_type != base_segment_type: + return False + + if predefined_type not in not_defined_values and predefined_type != base_predefined_type: + return False + + if system_type not in not_defined_values and system_type != base_system_type: + return False + + return True + + # NOTE: I have a feeling that there are cases where order + # in which we're checking the segments is important + # but I couldn't pin it down exact cases + for test_segment_data in fitting_data[:]: + for base_segment_data in segments_data: + if not are_segments_compatible(test_segment_data, base_segment_data): + continue + segments_data.remove(test_segment_data) + + # all segments were sorted + return len(segments_data) == 0 + + def pack_return_data(fitting_type, ports, segments_data): + for port in ports: + port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates) + if tool.Cad.is_x(port_local_position.length, 0.0): + start_port = port + break + connected_port = tool.System.get_connected_port(start_port) + connected_element = tool.System.get_port_relating_element(connected_port) + element_type = ifcopenshell.util.element.get_type(connected_element) + return {"fitting_type": fitting_type, "start_port_match": element_type == segments_data[0][0]} + + fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segments[0], "FittingType")) for fitting_type in fitting_types: if fitting_type.PredefinedType != predefined_type: continue @@ -315,13 +373,24 @@ class MEPGenerator: if not fittings: continue fitting = fittings[0] - elements = set( - ifcopenshell.util.system.get_connected_to(fitting) - + ifcopenshell.util.system.get_connected_from(fitting) - ) - for element in elements: - if element.IsTypedBy and element.IsTypedBy[0].RelatingType == segment_type: - return fitting_type + + ports = ifcopenshell.util.system.get_ports(fitting) + fitting_data = [] + fitting_connected_to_none_type = False + for port in ports: + connected_port = tool.System.get_connected_port(port) + connected_element = tool.System.get_port_relating_element(connected_port) + element_type = ifcopenshell.util.element.get_type(connected_element) + if element_type is None: + fitting_connected_to_none_type = True + break + fitting_data.append((element_type, port.PredefinedType, port.SystemType)) + + if fitting_connected_to_none_type: + continue + + if are_connected_elements_compatible(segments_data, fitting_data): + return pack_return_data(fitting_type, ports, segments_data) def create_obstruction_type(self, segment): # code is very similar to "bim.add_type" @@ -333,7 +402,8 @@ class MEPGenerator: ifc_file = tool.Ifc.get() body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") - obj = bpy.data.objects.new("Fitting", None) + obj = bpy.data.objects.new("Obstruction", None) + # TODO: OBSTRUCTION predefined type is available only for IfcDuctFitting and IfcPipeFitting element = blenderbim.core.root.assign_class( tool.Ifc, tool.Collector, @@ -377,7 +447,8 @@ class MEPGenerator: segment_obj = tool.Ifc.get_object(segment) segment_matrix = segment_obj.matrix_world segment_rotation = segment_matrix.to_quaternion() - obstruction_type = self.get_compatible_fitting_type(segment, "OBSTRUCTION") + fitting_data = self.get_compatible_fitting_type(segment, related_port, "OBSTRUCTION") + obstruction_type = fitting_data["fitting_type"] if fitting_data else None if not obstruction_type: obstruction_type = self.create_obstruction_type(segment) @@ -389,17 +460,11 @@ class MEPGenerator: obstruction_obj.matrix_world = segment_matrix profile_joiner.set_depth(obstruction_obj, length) - obstruction = tool.Ifc.get_entity(obstruction_obj) - # TODO: specify PredefinedType based on the segment type - obstruction_port = tool.Ifc.run("system.add_port", element=obstruction) - obstruction_port.PredefinedType = self.get_port_predefined_type(obstruction) - port_local_position = Matrix.Translation((0, 0, length)) if at_segment_start else Matrix() - tool.Ifc.run( - "geometry.edit_object_placement", - product=obstruction_port, - matrix=segment_matrix @ port_local_position, - is_si=True, - ) + obstruction_port = tool.System.add_ports( + obstruction_obj, + add_start_port=not at_segment_start, + add_end_port=at_segment_start, + )[0] # change segment length new_segment_length = segment_data["extrusion_depth"] - length @@ -411,6 +476,7 @@ class MEPGenerator: obstruction_obj.location += segment_rotation @ V(0, 0, new_segment_length) tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="NOTDEFINED") + obstruction = tool.Ifc.get_entity(obstruction_obj) return obstruction, None @@ -449,3 +515,153 @@ class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator): return {"CANCELLED"} return {"FINISHED"} + + +class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.mep_add_transition" + bl_label = "Add Transition" + bl_description = ( + "Adds transition between two MEP elements. Elements are either provided by ID or selected in Blender" + ) + bl_options = {"REGISTER", "UNDO"} + start_length: bpy.props.FloatProperty( + name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE" + ) + end_length: bpy.props.FloatProperty( + name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE" + ) + start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0) + end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0) + + def _execute(self, context): + start_element, end_element = None, None + ifc_file = tool.Ifc.get() + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + + if self.start_segment_id and self.end_segment_id: + start_element = ifc_file.by_id(self.start_segment_id) + end_element = ifc_file.by_id(self.end_segment_id) + start_object = tool.Ifc.get_object(start_element) + end_object = tool.Ifc.get_object(end_element) + + elif len(context.selected_objects) == 2: + start_object = context.active_object + end_object = next(o for o in context.selected_objects if o != context.active_object) + start_element = tool.Ifc.get_entity(start_object) + end_element = tool.Ifc.get_entity(end_object) + if not start_element or not end_element: + self.report({"ERROR"}, f"Two IFC elements should be selected for the transition") + return {"CANCELLED"} + + else: + self.report({"ERROR"}, f"Two IFC elements should be provided for the transition") + return {"CANCELLED"} + + # TODO: support IfcFlowTerminal + def is_mep(element): + return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting") + + if not is_mep(start_element) or not is_mep(end_element): + self.report( + {"ERROR"}, + f"Failed to add transition - some object is not a MEP element: {start_element.is_a()}, {end_element.is_a()}.", + ) + return {"CANCELLED"} + + start_axis = tool.Model.get_flow_segment_axis(start_object) + end_axis = tool.Model.get_flow_segment_axis(end_object) + + # TODO: support cases when segments are partially or completely overlapping each other + if not tool.Cad.are_edges_collinear(start_axis, end_axis): + self.report({"ERROR"}, f"Failed to add transition - non collinear segments are not yet supported.") + return {"CANCELLED"} + + start_segment_data = MEPGenerator().get_segment_data(start_element) + end_segment_data = MEPGenerator().get_segment_data(end_element) + end_port = end_segment_data["start_port"] + start_port = start_segment_data["end_port"] + + points_ports_map = { + start_segment_data["start_point"]: start_segment_data["start_port"], + start_segment_data["end_point"]: start_segment_data["end_port"], + end_segment_data["start_point"]: end_segment_data["start_port"], + end_segment_data["end_point"]: end_segment_data["end_port"], + } + + start_point, end_point = tool.Cad.closest_points( + (start_segment_data["start_point"], start_segment_data["end_point"]), + (end_segment_data["start_point"], end_segment_data["end_point"]), + ) + transition_dir = (end_point - start_point).normalized() + start_port = points_ports_map[start_point] + end_port = points_ports_map[end_point] + + # add transition representation + builder = ShapeBuilder(ifc_file) + rep, transition_data = builder.mep_transition_shape( + start_element, end_element, self.start_length / si_conversion, self.end_length / si_conversion + ) + + if not rep: + self.report({"ERROR"}, f"Failed to add transition - this kind of profiles is not yet supported.") + return {"CANCELLED"} + + middle_point = (start_point + end_point) / 2 + full_transition_length = transition_data["full_transition_length"] * si_conversion + start_segment_extend_point = middle_point - transition_dir * full_transition_length / 2 + end_segment_extend_point = middle_point + transition_dir * full_transition_length / 2 + DumbProfileJoiner().join_E(start_object, start_segment_extend_point) + DumbProfileJoiner().join_E(end_object, end_segment_extend_point) + + fitting_data = MEPGenerator().get_compatible_fitting_type( + [start_element, end_element], [start_port, end_port], "TRANSITION" + ) + + transition_type = fitting_data["fitting_type"] if fitting_data else None + start_port_match = fitting_data["start_port_match"] if fitting_data else True + + if not transition_type: + mesh = bpy.data.meshes.new("Transition") + obj = bpy.data.objects.new("Transition", mesh) + transition_type = blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class=MEPGenerator().get_mep_element_class_name(start_element, "FittingType"), + predefined_type="TRANSITION", + should_add_representation=False, + ) + body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + tool.Model.replace_object_ifc_representation(body, obj, rep) + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting") + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Data": json.dumps(transition_data, default=list)}, + ) + + # NOTE: at this point we loose current blender objects selection + bpy.ops.bim.add_constr_type_instance(relating_type_id=transition_type.id()) + transition_obj = bpy.context.active_object + + # adjust transition segment rotation and location + transition_obj.matrix_world = start_object.matrix_world + context.view_layer.update() + transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj)) + direction_match = tool.Cad.are_vectors_equal(transition_obj_dir, transition_dir) + + # if there are no mismatches or everything matches up we don't need to flip the transition + if start_port_match != direction_match: + transition_obj.matrix_world = start_object.matrix_world @ Matrix.Rotation(radians(180), 4, "X") + transition_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point + + # add ports and connect them + ports = tool.System.add_ports(transition_obj) + if not start_port_match: + start_port, end_port = end_port, start_port + tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED") + tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED") + + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/tool/cad.py b/src/blenderbim/blenderbim/tool/cad.py index 9ed290e59a..6a28231b74 100644 --- a/src/blenderbim/blenderbim/tool/cad.py +++ b/src/blenderbim/blenderbim/tool/cad.py @@ -91,10 +91,14 @@ class Cad: tolerance = VTX_PRECISION if isinstance(x, (list, tuple)): for y in x: - if value > (y - tolerance) and value < (y + tolerance): + if (y + tolerance) > value > (y - tolerance): return True return False - return value > (x - tolerance) and value < (x + tolerance) + return (x + tolerance) > value > (x - tolerance) + + @classmethod + def are_vectors_equal(cls, v1: Vector, v2: Vector): + return cls.is_x((v2 - v1).length, 0) @classmethod def intersect_edges(cls, edge1, edge2): @@ -227,6 +231,48 @@ class Cad: res = [cls.is_point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]] return len([i for i in res if i]) + @classmethod + def get_edge_direction(cls, edge): + return (edge[1] - edge[0]).normalized() + + @classmethod + def are_edges_collinear(cls, edge1, edge2): + def is_point_on_line(p, edge): + a1, a2 = edge + # comparing slopes between PA1 and A2A1 + # using cross multiplication to avoid division by zero + return cls.is_x((p.y - a1.y) * (a2.x - a1.x), (a2.y - a1.y) * (p.x - a1.x)) + + edge1_dir = edge1[1] - edge1[0] + edge2_dir = edge2[1] - edge2[0] + + if cls.is_x(edge1_dir.cross(edge2_dir).length_squared, 0): # check they are parallel + if is_point_on_line(edge1[0], edge2) or is_point_on_line(edge1[1], edge2): + return True + return False + + @classmethod + def closest_points(cls, edge1, edge2): + """ + + closest end points between `edge1` and `edge2` assuming `edge1` and `edge2` are collinear. + + < returns two points, first one belongs to `edge1` and second to `edge2` + + """ + direction = (edge1[1] - edge1[0]).normalized() + + # Project points onto the line to get scalar values along the direction + points1_values = [(p, p.dot(direction)) for p in edge1] + points2_values = [(p, p.dot(direction)) for p in edge2] + + # Sort the projections for both edges + sorted_points1 = sorted(points1_values, key=lambda el: el[1]) + sorted_points2 = sorted(points2_values, key=lambda el: el[1]) + + # The closest points will be the last point of the first edge and the first point of the second edge + return sorted_points1[-1][0], sorted_points2[0][0] + @classmethod def find_intersecting_edges(cls, bm, pt, idx1, idx2): """ diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index e6aa4ecabc..5796fcafa0 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -21,9 +21,35 @@ import ifcopenshell.util.system import blenderbim.core.tool import blenderbim.tool as tool from blenderbim.bim import import_ifc +import re +from mathutils import Matrix class System(blenderbim.core.tool.System): + @classmethod + def add_ports(cls, obj, add_start_port=True, add_end_port=True): + def add_port(mep_element, matrix): + port = tool.Ifc.run("system.add_port", element=mep_element) + port.FlowDirection = "NOTDEFINED" + port.PredefinedType = tool.System.get_port_predefined_type(mep_element) + tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=matrix, is_si=True) + return port + + # make sure obj.dimensions and .matrix_world has valid data + bpy.context.view_layer.update() + # need to make sure .ObjectPlacement is also updated when we're going to add ports + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + + mep_element = tool.Ifc.get_entity(obj) + length = obj.dimensions.z + ports = [] + if add_start_port: + ports.append(add_port(mep_element, obj.matrix_world @ Matrix())) + if add_end_port: + ports.append(add_port(mep_element, obj.matrix_world @ Matrix.Translation((0, 0, length)))) + return ports + @classmethod def create_empty_at_cursor_with_element_orientation(cls, element): element_obj = tool.Ifc.get_object(element) @@ -68,6 +94,14 @@ class System(blenderbim.core.tool.System): def get_port_relating_element(cls, port): return port.Nests[0].RelatingObject + @classmethod + def get_port_predefined_type(cls, mep_element): + split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x) + class_name = "".join(split_camel_case(mep_element.is_a())[1:-1]).upper() + if class_name == "CONVEYOR": + return "NOTDEFINED" + return class_name + @classmethod def import_system_attributes(cls, system): props = bpy.context.scene.BIMSystemProperties diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index fdcd02815c..6cf52d0581 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -19,7 +19,7 @@ import collections import ifcopenshell import ifcopenshell.api -from math import cos, sin, pi +from math import cos, sin, pi, tan, radians from mathutils import Vector, Matrix from itertools import chain @@ -539,7 +539,7 @@ class ShapeBuilder: "Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation" ) - if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"): + if not profile_or_curve.is_a("IfcProfileDef"): profile_or_curve = self.profile(profile_or_curve) if position_y_axis: @@ -579,6 +579,8 @@ class ShapeBuilder: representation_type = "AdvancedSweptSolid" elif "IfcExtrudedAreaSolid" in item_types: representation_type = "SweptSolid" + elif items[0].is_a("IfcTessellatedItem"): + representation_type = "Tessellation" elif items[0].is_a("IfcCurve") and items[0].Dim == 3: representation_type = "Curve3D" else: @@ -746,8 +748,10 @@ class ShapeBuilder: ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments) return (points, segments, ifc_curve) - - def create_z_profile_lips_curve(self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius): + + def create_z_profile_lips_curve( + self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius + ): x1 = FirstFlangeWidth x2 = SecondFlangeWidth y = Depth / 2 @@ -770,20 +774,21 @@ class ShapeBuilder: (-x1+t, -y+t), (-t/2, -y+t) ) - # fmt: on # option for no additional thickness in outer radius: # points, segments, ifc_curve = create_curve_from_coords( # coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=r, closed=True, ifc_file=ifc_file # ) - points, segments, ifc_curve = self.get_simple_2dcurve_data(coords, + points, segments, ifc_curve = self.get_simple_2dcurve_data( + coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r), closed=True, create_ifc_curve=True) + # fmt: on return ifc_curve - + def create_transition_arc_ifc(self, width, height, create_ifc_curve=False): # create an arc in the rectangle with specified width and height # if it's not possible to make a complete arc @@ -814,4 +819,114 @@ class ShapeBuilder: points, segments, transition_arc = self.get_simple_2dcurve_data( curve_coords, fillets, fillet_radius, closed=False, create_ifc_curve=create_ifc_curve ) - return points, segments, transition_arc \ No newline at end of file + return points, segments, transition_arc + + def polygonal_face_set(self, points, faces): + """ + > `points` - list of points + + > `faces` - list of faces consisted of point indices (points indices starting from 0) + + < IfcPolygonalFaceSet + """ + + ifc_points = self.file.createIfcCartesianPointList3D(points) + ifc_faces = [] + for face in faces: + face = [i + 1 for i in face] + ifc_faces.append(self.file.createIfcIndexedPolygonalFace(face)) + + face_set = self.file.createIfcPolygonalFaceSet(Coordinates=ifc_points, Faces=ifc_faces) + + return face_set + + def mep_transition_shape(self, start_segment, end_segment, start_length, end_length, angle=30.0): + """ + returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data + """ + # good default values from angle = 30/60 deg + # 30 degree angle will result in 75 degrees on the transition (= 90 - α/2) - https://i.imgur.com/tcoYDWu.png + + # TODO: get rid of reliance on profiles + def get_profile(element): + material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1: + return material.MaterialProfiles[0].Profile + + start_profile = get_profile(start_segment) + end_profile = get_profile(end_segment) + + # TODO: support more profiles + if not start_profile.is_a("IfcRectangleProfileDef") or not end_profile.is_a("IfcRectangleProfileDef"): + # Non rectangular profiles are not yet supported + return None, None + + start_half_dim = V(start_profile.XDim / 2, start_profile.YDim / 2, start_length) + end_half_dim = V(end_profile.XDim / 2, end_profile.YDim / 2, end_length) + + transition_items = [] + end_extrusion_offset = V(0, 0, start_length) + + def get_transition_legth(start_half_dim, end_half_dim, angle): + diff = start_half_dim.xy - end_half_dim.xy + diff = Vector([abs(i) for i in diff]) + c = diff.x * tan(radians(90 - angle / 2)) + a = diff.y + b = (c**2 - a**2) ** 0.5 + return b + + transition_length = get_transition_legth(start_half_dim, end_half_dim, angle) + faces = [] + if transition_length != 0: + end_extrusion_offset.z += transition_length + + faces += [(3, 4, 7, 0), (11, 8, 15, 12), (3, 11, 12, 4), (7, 15, 8, 0)] + + # NOTE: clockwise order for correct face orientation + faces += [ + # start extrusion + (0, 1, 2, 3), + (8, 11, 10, 9), + (0, 8, 9, 1), + (1, 9, 10, 2), + (2, 10, 11, 3), + # end extrusion + (4, 5, 6, 7), + (12, 15, 14, 13), + (4, 12, 13, 5), + (5, 13, 14, 6), + (6, 14, 15, 7), + ] + points = [ + start_half_dim * V(-1, -1, 1), + start_half_dim * V(-1, -1, 0), + start_half_dim * V(1, -1, 0), + start_half_dim * V(1, -1, 1), + end_half_dim * V(1, -1, 0) + end_extrusion_offset, + end_half_dim * V(1, -1, 1) + end_extrusion_offset, + end_half_dim * V(-1, -1, 1) + end_extrusion_offset, + end_half_dim * V(-1, -1, 0) + end_extrusion_offset, + start_half_dim * V(-1, 1, 1), + start_half_dim * V(-1, 1, 0), + start_half_dim * V(1, 1, 0), + start_half_dim * V(1, 1, 1), + end_half_dim * V(1, 1, 0) + end_extrusion_offset, + end_half_dim * V(1, 1, 1) + end_extrusion_offset, + end_half_dim * V(-1, 1, 1) + end_extrusion_offset, + end_half_dim * V(-1, 1, 0) + end_extrusion_offset, + ] + + face_set = self.polygonal_face_set(points, faces) + transition_items.append(face_set) + + body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW") + representation = self.get_representation(body, transition_items, "Tesselation") + transition_data = { + "start_length": start_length, + "end_length": end_length, + "angle": angle, + "transition_length": transition_length, + "full_transition_length": start_length + transition_length + end_length, + } + + return representation, transition_data From 29d4a8d5fe2386aa9ba052afb573fed0473cc370 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 9 Aug 2023 11:16:57 +0500 Subject: [PATCH 10/74] More Ports UI Created more UI for ports. Now you can see the list of ports from UI and what objects they are connected to, you can quickly select any ports/connected objects or disconnect objects. From port UI you can see what object's it located on and what object it's connected to and quickly jump across them. Demo - https://imgur.com/a/Gu4Vz7V --- .../blenderbim/bim/module/system/data.py | 40 +++++++ .../blenderbim/bim/module/system/operator.py | 8 +- .../blenderbim/bim/module/system/ui.py | 106 ++++++++++++++---- 3 files changed, 130 insertions(+), 24 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py index 5624f3cd87..6c71e88dc9 100644 --- a/src/blenderbim/blenderbim/bim/module/system/data.py +++ b/src/blenderbim/blenderbim/bim/module/system/data.py @@ -91,8 +91,15 @@ class PortData: @classmethod def load(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + cls.element = element + is_port = cls.is_port() cls.data = { "total_ports": cls.total_ports(), + "located_ports_data": cls.located_ports_data(), + "is_port": is_port, + "port_connected_object": cls.port_connected_object() if is_port else None, + "port_relating_object": cls.port_relating_object() if is_port else None, } cls.is_loaded = True @@ -100,3 +107,36 @@ class PortData: def total_ports(cls): element = tool.Ifc.get_entity(bpy.context.active_object) return len(ifcopenshell.util.system.get_ports(element)) + + @classmethod + def is_port(cls): + return cls.element and cls.element.is_a("IfcDistributionPort") + + @classmethod + def port_relating_object(cls): + return tool.Ifc.get_object(tool.System.get_port_relating_element(cls.element)) + + @classmethod + def port_connected_object(cls): + connected_port = tool.System.get_connected_port(cls.element) + if not connected_port: + return + connected_element = tool.System.get_port_relating_element(connected_port) + return tool.Ifc.get_object(connected_element) + + @classmethod + def located_ports_data(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + ports = ifcopenshell.util.system.get_ports(element) + + data = [] + for port in ports: + port_obj = tool.Ifc.get_object(port) + connected_port = tool.System.get_connected_port(port) + if connected_port: + connected_element = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port)) + else: + connected_element = None + + data.append((port, port_obj, connected_element)) + return data diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index 828977060e..00b86936e3 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -205,8 +205,14 @@ class DisconnectPort(bpy.types.Operator, Operator): bl_label = "Disconnect Ports" bl_options = {"REGISTER", "UNDO"} + element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) + def _execute(self, context): - core.disconnect_port(tool.Ifc, port=tool.Ifc.get_entity(context.active_object)) + if self.element_id != 0: + element = tool.Ifc.get().by_id(self.element_id) + else: + element = tool.Ifc.get_entity(context.active_object) + core.disconnect_port(tool.Ifc, port=element) class SetFlowDirection(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index c59b41c043..643d10ea93 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -23,6 +23,14 @@ from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, PortData +FLOW_DIRECTION_TO_ICON = { + "SOURCE": "FORWARD", + "SINK": "BACK", + "SOURCEANDSINK": "ARROW_LEFTRIGHT", + "NOTDEFINED": "RESTRICT_INSTANCED_ON", +} + + class BIM_PT_systems(Panel): bl_label = "Systems" bl_idname = "BIM_PT_systems" @@ -156,11 +164,43 @@ class BIM_PT_ports(Panel): self.props = context.scene.BIMSystemProperties row = self.layout.row(align=True) - row.label(text=f"{PortData.data['total_ports']} Ports Found", icon="PLUGIN") + total_ports = PortData.data["total_ports"] + row.label(text=f"{total_ports} Ports Found", icon="PLUGIN") row.operator("bim.show_ports", icon="HIDE_OFF", text="") row.operator("bim.hide_ports", icon="HIDE_ON", text="") row.operator("bim.add_port", icon="ADD", text="") + if total_ports == 0: + return + + row = self.layout.row(align=True) + row.label(text="Ports located on object and connected objects:") + row = self.layout.row(align=True) + cols = [row.column(align=True) for i in range(6)] + + for i, port_data in enumerate(PortData.data["located_ports_data"]): + port, port_obj, connected_obj = port_data + flow_direction_icon = FLOW_DIRECTION_TO_ICON[port.FlowDirection or "NOTDEFINED"] + if port_obj: + cols[0].label(text="", icon=flow_direction_icon) + cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port.id() + cols[2].label(text=port_obj.name) + else: + cols[0].label(text="", icon=flow_direction_icon) + cols[1].label(text="", icon="HIDE_ON") + cols[2].label(text="Port is hidden") + + if connected_obj: + cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id() + cols[4].operator( + "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" + ).ifc_id = connected_obj.BIMObjectProperties.ifc_definition_id + cols[5].label(text=f"{connected_obj.name}") + else: + cols[3].label(text="", icon="UNLINKED") + cols[4].label(text="", icon="BLANK1") + cols[5].label(text="Port is disconnected") + class BIM_PT_port(Panel): bl_label = "Port" @@ -184,36 +224,56 @@ class BIM_PT_port(Panel): def draw(self, context): self.props = context.scene.BIMSystemProperties - element = tool.Ifc.get_entity(context.active_object) - port_class = element.is_a() layout = self.layout row = layout.row(align=True) - row.label(text=port_class) + row.label(text="IfcDistributionPort") row.operator("bim.connect_port", icon="PLUGIN", text="") row.operator("bim.disconnect_port", icon="UNLINKED", text="") row.operator("bim.remove_port", icon="X", text="") - if port_class == "IfcDistributionPort": - current_flow_direction = str(element.FlowDirection) - row = layout.row(align=True) - row.label(text="Flow Direction:") - row.label(text=current_flow_direction) + if not PortData.is_loaded: + PortData.load() - # TODO: replace with enum property? - flow_directions = ( - ("SOURCE", "FORWARD"), - ("SINK", "BACK"), - ("SOURCEANDSINK", "ARROW_LEFTRIGHT"), - ("NOTDEFINED", "RESTRICT_INSTANCED_ON"), - ) + if not PortData.data["is_port"]: + return - row = layout.row(align=True) - row.label(text="Change Flow Direction:") - for flow_direction, icon in flow_directions: - row = layout.row() - row.operator("bim.set_flow_direction", icon=icon, text=flow_direction).direction = flow_direction - if flow_direction == current_flow_direction: - row.enabled = False + element = tool.Ifc.get_entity(context.active_object) + current_flow_direction = str(element.FlowDirection) + row = layout.row(align=True) + row.label(text="Flow Direction:") + row.label(text=current_flow_direction) + + # port located on + row = layout.row(align=True) + relating_object = PortData.data["port_relating_object"] + row.label(text="Port located on:") + row.label(text=relating_object.name) + row.operator( + "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" + ).ifc_id = relating_object.BIMObjectProperties.ifc_definition_id + + # object connected to the port + row = layout.row(align=True) + connected_object = PortData.data["port_connected_object"] + if connected_object: + row.label(text="Port connected to:") + row.label(text=connected_object.name) + row.operator( + "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" + ).ifc_id = connected_object.BIMObjectProperties.ifc_definition_id + else: + row.label(text="Port is not connected to any element") + + # TODO: replace with enum property? + row = layout.row(align=True) + row.label(text="Change Flow Direction:") + for flow_direction in FLOW_DIRECTION_TO_ICON.keys(): + row = layout.row() + row.operator( + "bim.set_flow_direction", icon=FLOW_DIRECTION_TO_ICON[flow_direction], text=flow_direction + ).direction = flow_direction + if flow_direction == current_flow_direction: + row.enabled = False class BIM_UL_systems(UIList): From f8ffc97bf5b82d41e7ba9579e1ebe9905f6212b8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 9 Aug 2023 16:30:55 +0500 Subject: [PATCH 11/74] remove_product to also remove psets for types --- .../ifcopenshell/api/root/remove_product.py | 24 +++++++++++++++---- .../test/api/root/test_remove_product.py | 18 ++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py index c3577a43e0..0ec81a87d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py @@ -65,13 +65,29 @@ class Usecase: representations = self.settings["product"].Representation.Representations or [] else: representations = [] - + + # remove object placements object_placement = self.settings["product"].ObjectPlacement - if object_placement and self.file.get_total_inverses(object_placement) == 1: - self.settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work - ifcopenshell.util.element.remove_deep2(self.file, object_placement) + if object_placement: + if self.file.get_total_inverses(object_placement) == 1: + self.settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work + ifcopenshell.util.element.remove_deep2(self.file, object_placement) + elif self.settings["product"].is_a("IfcTypeProduct"): representations = [rm.MappedRepresentation for rm in self.settings["product"].RepresentationMaps or []] + + # remove psets + psets = self.settings["product"].HasPropertySets or [] + for pset in psets: + if self.file.get_total_inverses(pset) != 1: + continue + ifcopenshell.api.run( + "pset.remove_pset", + self.file, + product=self.settings["product"], + pset=pset, + ) + for representation in representations: ifcopenshell.api.run( "geometry.unassign_representation", diff --git a/src/ifcopenshell-python/test/api/root/test_remove_product.py b/src/ifcopenshell-python/test/api/root/test_remove_product.py index d45b306e76..eb8a0d6631 100644 --- a/src/ifcopenshell-python/test/api/root/test_remove_product.py +++ b/src/ifcopenshell-python/test/api/root/test_remove_product.py @@ -54,6 +54,24 @@ class TestRemoveProduct(test.bootstrap.IFC4): ifcopenshell.api.run("root.remove_product", self.file, product=element1) assert len(self.file.by_type("IfcObjectPlacement")) == 0 + def test_removing_element_type_psets(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"}) + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2.HasPropertySets = (pset,) + + # make sure it won't remove the pset if it's connected elsewhere + ifcopenshell.api.run("root.remove_product", self.file, product=element2) + assert len(self.file.by_type("IfcPropertySet")) == 1 + assert len(self.file.by_type("IfcPropertySingleValue")) == 1 + + # if it's the product is the only inverse for pset, it should remove the pset + ifcopenshell.api.run("root.remove_product", self.file, product=element) + assert len(self.file.by_type("IfcPropertySet")) == 0 + assert len(self.file.by_type("IfcPropertySingleValue")) == 0 + def test_removing_all_representations_of_an_element(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") ifcopenshell.api.run("unit.assign_unit", self.file) From b78490ca56ae8ed3871f68510945a6d77e13ece9 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 10 Aug 2023 10:52:06 +0800 Subject: [PATCH 12/74] #3510 Sample points along diagonal as well for quick and dirty occlusion detection --- src/serializers/SvgSerializer.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index 1424c1420e..19ade12ee6 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -309,11 +309,16 @@ namespace { } } else { gp_Pnt2d tmp; - for (int i = 0; i < 4; ++i) { + // 0,1,2,3 -> interp over bounding box edges (i%4, (i+1)%4) + // 4,5 -> interp over bounding box diagonals (i%4, (i+2)%4) + // @todo use boolean_utils.h points_on_planar_face_generator? + // ... or skip faces with inner bounds all together ? + // ... ? + for (int i = 0; i < 6; ++i) { // @todo proper edge intersection for (int j = 0; j < 16; ++j) { - const gp_Pnt2d& a = *loop[i]; - const gp_Pnt2d& b = *loop[(i + 1) % 4]; + const gp_Pnt2d& a = *loop[i % 4]; + const gp_Pnt2d& b = *loop[(i + (i >= 4 ? 2 : 1)) % 4]; interp(a, b, j / 16.0, tmp); if (fclass->Perform(tmp) == TopAbs_OUT) { return false; From a05059db01f71f908eb85443d9b834cdbc20b623 Mon Sep 17 00:00:00 2001 From: Kristoffer Andersen Date: Thu, 10 Aug 2023 08:06:38 +0200 Subject: [PATCH 13/74] Update build.sh Reduce number of schema versions --- conda/build.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda/build.sh b/conda/build.sh index 77c9d54e8f..afb977d53a 100644 --- a/conda/build.sh +++ b/conda/build.sh @@ -12,6 +12,8 @@ if [ `uname` == Darwin ]; then export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress" fi +export SCHEMA_VERSIONS=2x3;4;4x3;4x3_add1 + cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=$PREFIX \ From 7827ce3448adf7b33530dd4268713c0cf186069f Mon Sep 17 00:00:00 2001 From: Kristoffer Andersen Date: Thu, 10 Aug 2023 08:22:07 +0200 Subject: [PATCH 14/74] Update build.sh fix env var declaration --- conda/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda/build.sh b/conda/build.sh index afb977d53a..adeeb58d95 100644 --- a/conda/build.sh +++ b/conda/build.sh @@ -12,7 +12,7 @@ if [ `uname` == Darwin ]; then export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress" fi -export SCHEMA_VERSIONS=2x3;4;4x3;4x3_add1 +export SCHEMA_VERSIONS="2x3;4;4x3;4x3_add1" cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ From d5d1b3567bf4613e5b4b5cb43f3604abd39ed77d Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 10 Aug 2023 15:44:21 +0800 Subject: [PATCH 15/74] Update draw.py --include-curves --- src/ifcopenshell-python/ifcopenshell/draw.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/draw.py b/src/ifcopenshell-python/ifcopenshell/draw.py index 3e1a25d149..df4169ddd8 100644 --- a/src/ifcopenshell-python/ifcopenshell/draw.py +++ b/src/ifcopenshell-python/ifcopenshell/draw.py @@ -60,6 +60,7 @@ class draw_settings: merge_cells: bool = False include_projection: bool = True prefilter: bool = True + include_curves: bool = False def main(settings, files, iterators=None, merge_projection=True, progress_function=DO_NOTHING): @@ -68,6 +69,7 @@ def main(settings, files, iterators=None, merge_projection=True, progress_functi # this is required for serialization APPLY_DEFAULT_MATERIALS=True, DISABLE_TRIANGULATION=True, + INCLUDE_CURVES=settings.include_curves, # when not doing booleans, proper solids from shells isn't a requirement SEW_SHELLS=settings.subtract_before_hlr, ) From 4d49517111967701b60f6b79539a9b00413d2006 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 10 Aug 2023 13:49:35 +0500 Subject: [PATCH 16/74] small refactor --- src/blenderbim/blenderbim/bim/module/model/roof.py | 14 +++++--------- src/blenderbim/blenderbim/tool/drawing.py | 3 +-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/roof.py b/src/blenderbim/blenderbim/bim/module/model/roof.py index 06a1b67069..cb7a1a30d1 100644 --- a/src/blenderbim/blenderbim/bim/module/model/roof.py +++ b/src/blenderbim/blenderbim/bim/module/model/roof.py @@ -40,10 +40,6 @@ from pprint import pprint # https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.htm -def float_is_zero(f): - return 0.0001 >= f >= -0.0001 - - def bm_mesh_clean_up(bm): # remove internal edges and faces # adding missing faces so we could rely on `e.is_boundary` later @@ -99,7 +95,7 @@ def is_valid_roof_footprint(bm): # should be bmesh to support edit mode bm.verts.ensure_lookup_table() base_z = bm.verts[0].co.z - all_verts_same_level = all([float_is_zero(v.co.z - base_z) for v in bm.verts[1:]]) + all_verts_same_level = all([tool.Cad.is_x(v.co.z - base_z, 0) for v in bm.verts[1:]]) if not all_verts_same_level: return ( {"ERROR"}, @@ -202,7 +198,7 @@ def generate_hiped_roof_bmesh( def find_identical_new_vert(co): for v in bm.verts: - if float_is_zero((co - v.co).length): + if tool.Cad.is_x((co - v.co).length, 0): return v def find_other_polygon_verts(edge): @@ -234,7 +230,7 @@ def generate_hiped_roof_bmesh( bottom_chords_to_remove = [] def is_footprint_vert(v): - return float_is_zero(v.co.z - footprint_z) + return tool.Cad.is_x(v.co.z - footprint_z, 0) def is_footprint_edge(edge): return all(is_footprint_vert(v) for v in edge.verts) @@ -326,7 +322,7 @@ def generate_hiped_roof_bmesh( default_offset_dir = Vector([0, 0, 1]) * roof_thickness footprint_verts = set() - if not float_is_zero(rafter_edge_angle): + if not tool.Cad.is_x(rafter_edge_angle, 0): footprint_edges = [] for edge in extruded_edges: if is_footprint_edge(edge): @@ -398,7 +394,7 @@ def update_roof_modifier_ifc_data(context): if not angle_layer: return False for edge_angle in angle_layer: - if float_is_zero(edge_angle - pi / 2): + if tool.Cad.is_x(edge_angle - pi / 2, 0): return True return False diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 73f24747d9..6ad4ba21a7 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -122,11 +122,10 @@ class Drawing(blenderbim.core.tool.Drawing): # place the arrow # NOTE: may not work correctly in EDIT mode bbox = tool.Blender.get_object_bounding_box(stair) - float_is_zero = lambda f: 0.0001 >= f >= -0.0001 arrow.location = stair.matrix_world @ Vector( (bbox["min_x"], (bbox["max_y"] - bbox["min_y"]) / 2, bbox["max_z"]) ) - last_step_x = max(v.co.x for v in stair.data.vertices if float_is_zero(v.co.z - bbox["max_z"])) + last_step_x = max(v.co.x for v in stair.data.vertices if tool.Cad.is_x(v.co.z - bbox["max_z"], 0)) arrow.data.splines[0].points[0].co = Vector((0, 0, 0, 1)) arrow.data.splines[0].points[1].co = Vector((last_step_x, 0, 0, 1)) From a51ecea9b184f9c1e9567b433a92b74624e87598 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 10 Aug 2023 13:55:46 +0500 Subject: [PATCH 17/74] More descriptive error message on attempt to delete drawings category --- src/blenderbim/blenderbim/bim/module/drawing/operator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 2717eae142..1cbbd8d838 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -1512,6 +1512,9 @@ class RemoveDrawing(bpy.types.Operator, Operator): tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected ] else: + if not self.drawing: + self.report({"ERROR"}, "No drawing selected") + return {"CANCELLED"} drawings = [tool.Ifc.get().by_id(self.drawing)] removed_drawings = [drawing.id() for drawing in drawings] From a8888efb8832b53dcf1eeccbef2de17ed1b90e1c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 10 Aug 2023 14:12:20 +0500 Subject: [PATCH 18/74] fixed bug getting port relating element in ifc2x3 --- src/blenderbim/blenderbim/tool/system.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 5796fcafa0..3bb15d5cb0 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -92,7 +92,11 @@ class System(blenderbim.core.tool.System): @classmethod def get_port_relating_element(cls, port): - return port.Nests[0].RelatingObject + if tool.Ifc.get_schema() == "IFC2X3": + element = port.ContainedIn[0].RelatedElement + else: + element = port.Nests[0].RelatingObject + return element @classmethod def get_port_predefined_type(cls, mep_element): From 631d8fcfb1ee1431c1eba03979b9594ca0f01da8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 10 Aug 2023 15:06:16 +0500 Subject: [PATCH 19/74] quickly connect mep elements if they have matching ports Demo - https://imgur.com/a/o9u8M1u --- .../blenderbim/bim/module/system/__init__.py | 1 + .../blenderbim/bim/module/system/operator.py | 41 +++++++++++++++++++ .../blenderbim/bim/module/system/ui.py | 1 + 3 files changed, 43 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py index 6cc772e135..a57b7fb5a8 100644 --- a/src/blenderbim/blenderbim/bim/module/system/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -34,6 +34,7 @@ classes = ( operator.RemovePort, operator.RemoveSystem, operator.SelectSystemProducts, + operator.MEPConnectElements, operator.SetFlowDirection, operator.ShowPorts, operator.UnassignSystem, diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index 00b86936e3..e3a35e3670 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -23,6 +23,7 @@ import blenderbim.core.system as core import blenderbim.bim.handler from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.system.data import PortData +from mathutils import Matrix class Operator: @@ -215,6 +216,46 @@ class DisconnectPort(bpy.types.Operator, Operator): core.disconnect_port(tool.Ifc, port=element) +class MEPConnectElements(bpy.types.Operator, Operator): + bl_idname = "bim.mep_connect_elements" + bl_label = "Connect MEP Elements" + bl_description = "Connects two selected elements if they have ports with matching location" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return len(context.selected_objects) == 2 + + def _execute(self, context): + obj1 = context.active_object + obj2 = next(o for o in context.selected_objects if o != obj1) + + el1 = tool.Ifc.get_entity(obj1) + el2 = tool.Ifc.get_entity(obj2) + + obj1_ports = [p for p in tool.System.get_ports(el1) if not tool.System.get_connected_port(p)] + obj2_ports = [p for p in tool.System.get_ports(el2) if not tool.System.get_connected_port(p)] + + if not obj1_ports or not obj2_ports: + self.report({"ERROR"}, "Couldn't find free ports to connect.") + return + + def get_element_matrix(element): + placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + return Matrix(placement) + + for port1 in obj1_ports: + port1_location = get_element_matrix(port1).translation + for port2 in obj2_ports: + port2_location = get_element_matrix(port2).translation + if tool.Cad.are_vectors_equal(port1_location, port2_location): + core.connect_port(tool.Ifc, port1, port2) + return {"FINISHED"} + + self.report({"ERROR"}, "Couldn't find any matching ports to connect.") + return {"CANCELLED"} + + class SetFlowDirection(bpy.types.Operator, Operator): bl_idname = "bim.set_flow_direction" bl_label = "Set Flow Direction" diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index 643d10ea93..b0b411e23a 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -166,6 +166,7 @@ class BIM_PT_ports(Panel): row = self.layout.row(align=True) total_ports = PortData.data["total_ports"] row.label(text=f"{total_ports} Ports Found", icon="PLUGIN") + row.operator("bim.mep_connect_elements", text="", icon="PLUGIN") row.operator("bim.show_ports", icon="HIDE_OFF", text="") row.operator("bim.hide_ports", icon="HIDE_ON", text="") row.operator("bim.add_port", icon="ADD", text="") From 2d847d1704ef5e1f86fcdefc4709f685f73db34d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 10 Aug 2023 16:30:51 +0500 Subject: [PATCH 20/74] Adjusting connected elements on bim.regenerate_distribution_element Basically commented out automatic adjustment after 45f81b478 - now it's hapenning only when you decide to regenerate it. It's adjusting connected segments extrusion and location, for all other elements besides segments it's adjusting only location. The idea is it will try to change as less as possible. Demo - https://imgur.com/oZ1I2Bl --- .../blenderbim/bim/module/model/mep.py | 63 ++++++++++++++++--- .../blenderbim/bim/module/system/operator.py | 8 +-- src/blenderbim/blenderbim/tool/model.py | 5 ++ 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index bcc45920c3..6aae52ac71 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -73,29 +73,68 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator): [e for e in ifcopenshell.util.system.get_connected_from(element) if e not in processed_elements] ) - if len(connected) == 1: - extend_branch(list(connected)[0], branch, element) - else: - for connected_element in connected: - branch_element["children"].append(extend_branch(connected_element, [], element)) + for connected_element in connected: + branch_element["children"].append(extend_branch(connected_element, [], element)) return branch - queue = extend_branch(current_element, [])[0]["children"] + extended_branch = extend_branch(current_element, []) + queue = extended_branch[0]["children"] # import pprint # pprint.pprint(queue) + def get_connected_ports_between(element1, element2): + ports1 = tool.System.get_ports(element1) + ports2 = tool.System.get_ports(element2) + + for p in ports1: + connected_port = tool.System.get_connected_port(p) + # in IFC2X3 there is no PredefinedType + if getattr(p, "PredefinedType", None) == "WIRELESS": + continue + if connected_port in ports2: + return p, connected_port + + return None, None + + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + def process_branch(branch): for branch_element in branch: element = branch_element["element"] print("processing", element) predecessor = branch_element["predecessor"] - if False: # If the element does not need to be transformed, return early. - return + # Perform the extend, translate, rotate, etc the element as necessary based on the predecessor. - # For segments, prioritise extensions instead of translations. - # For everything else, only translate. No rotation. + # For everything besides segments, only translate. No rotation. + + obj = tool.Ifc.get_object(element) + obj_pred = tool.Ifc.get_object(predecessor) + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + if tool.Ifc.is_moved(obj_pred): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj_pred) + + port, port_pred = get_connected_ports_between(element, predecessor) + port_matrix_pred = tool.Model.get_element_matrix(port_pred) + + # Only segments can be extended + # extension for them takes priority over translation + if element.is_a("IfcFlowSegment"): + DumbProfileJoiner().join_E(obj, port_matrix_pred.translation * si_conversion) + context.view_layer.update() # update since extrusion might involve changing object's location + + port_martix = tool.Model.get_element_matrix(port) + port_location = port_martix.translation + port_location_pred = port_matrix_pred.translation + if not tool.Cad.are_vectors_equal(port_location, port_location_pred): + obj.location += (port_location_pred - port_location) * si_conversion + context.view_layer.update() # otherwise tool.Ifc.is_moved won't get triggered + else: + # If the element does not need to be transformed, return early. + return + for child_branch in branch_element["children"]: process_branch(child_branch) @@ -229,6 +268,10 @@ class MEPGenerator: if port_position == "end_port": tool.Model.edit_element_placement(port, end_port_matrix) + continue + + # NOTE: currently this functionality is moved to bim.regenerate_distribution_element + connected_port = tool.System.get_connected_port(port) if not connected_port: continue diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index e3a35e3670..71336ec7fb 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -240,14 +240,10 @@ class MEPConnectElements(bpy.types.Operator, Operator): self.report({"ERROR"}, "Couldn't find free ports to connect.") return - def get_element_matrix(element): - placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) - return Matrix(placement) - for port1 in obj1_ports: - port1_location = get_element_matrix(port1).translation + port1_location = tool.Model.get_element_matrix(port1).translation for port2 in obj2_ports: - port2_location = get_element_matrix(port2).translation + port2_location = tool.Model.get_element_matrix(port2).translation if tool.Cad.are_vectors_equal(port1_location, port2_location): core.connect_port(tool.Ifc, port1, port2) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 705ca56710..a4e9295d72 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -768,6 +768,11 @@ class Model(blenderbim.core.tool.Model): return tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True) + @classmethod + def get_element_matrix(cls, element): + placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + return Matrix(placement) + @classmethod def reload_body_representation(cls, obj_or_objects): """Update body representation including all decomposed objects""" From 530d5a4e630210c7ae0c807355de6407d8783962 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 10 Aug 2023 17:37:52 +0500 Subject: [PATCH 21/74] plug add_transition to fit_flow_segments --- src/blenderbim/blenderbim/bim/module/model/mep.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index 6aae52ac71..4274e1c6cd 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -207,6 +207,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): elif total_profiles == 2: if is_parallel: fitting_type = "TRANSITION" + bpy.ops.bim.mep_add_transition() elif total_selected_objs == 3: if total_profiles > 1: From 7a0da7bd8efb8d1b6c3d7a65780d1b39f3a911c4 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:20:13 +0100 Subject: [PATCH 22/74] Revert "Rename and move sub panels of Costing and Schedule Tab" This reverts commit 9d4715c0a9ec15436e804d4a59b35cd092150c55. --- src/blenderbim/blenderbim/bim/__init__.py | 2 ++ src/blenderbim/blenderbim/bim/module/cost/ui.py | 9 +++++---- .../blenderbim/bim/module/resource/ui.py | 5 +++-- .../blenderbim/bim/module/sequence/ui.py | 16 +++++++++++----- src/blenderbim/blenderbim/bim/ui.py | 14 ++++++++++++++ 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 761ed00193..cfcd70e376 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -145,6 +145,8 @@ classes = [ ui.BIM_PT_tab_services_object, # Structural analysis ui.BIM_PT_tab_structural, + # Construction scheduling + ui.BIM_PT_tab_4D5D, # Facility management ui.BIM_PT_tab_handover, ui.BIM_PT_tab_operations, diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 351e9ae0e9..c083331c76 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -21,19 +21,21 @@ import blenderbim.bim.module.cost.prop as CostProp from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.cost.data import CostSchedulesData -import blenderbim.tool as tool class BIM_PT_cost_schedules(Panel): bl_label = "Cost Schedules" bl_idname = "BIM_PT_cost_schedules" + bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not CostSchedulesData.is_loaded: @@ -46,9 +48,8 @@ class BIM_PT_cost_schedules(Panel): row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT") row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT") else: - row.label(text="No Cost Schedules found.", icon="COMMUNITY") + row.label(text="No Cost Schedules Found found.", icon="COMMUNITY") row = self.layout.row() - row.alignment = "RIGHT" row.prop(self.props, "cost_schedule_predefined_types") row.operator("bim.add_cost_schedule", icon="ADD", text="Add") diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 351bb5631c..4ce36e12bf 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -20,7 +20,6 @@ import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.resource.data import ResourceData -import blenderbim.tool as tool class BIM_PT_resources(Panel): @@ -30,10 +29,12 @@ class BIM_PT_resources(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): self.props = context.scene.BIMResourceProperties diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 572977d379..eb0ccc477d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -22,7 +22,6 @@ from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.helper import draw_attributes from blenderbim.bim.module.sequence.data import WorkPlansData, WorkScheduleData, SequenceData, TaskICOMData -import blenderbim.tool as tool class BIM_PT_work_plans(Panel): @@ -32,10 +31,12 @@ class BIM_PT_work_plans(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and file.schema != "IFC2X3" def draw(self, context): if not WorkPlansData.is_loaded: @@ -98,13 +99,16 @@ class BIM_PT_work_plans(Panel): class BIM_PT_work_schedules(Panel): bl_label = "Work Schedules" bl_idname = "BIM_PT_work_schedules" + bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not SequenceData.is_loaded: @@ -127,7 +131,7 @@ class BIM_PT_work_schedules(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" row.prop(self.props, "work_schedule_predefined_types") - row.operator("bim.add_work_schedule", text="Add", icon="ADD") + row.operator("bim.add_work_schedule", text="Add new", icon="ADD") for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items(): @@ -813,10 +817,12 @@ class BIM_PT_work_calendars(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() and tool.Ifc.get().schema != "IFC2X3" + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not SequenceData.is_loaded: diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 58958f4b20..81debd7574 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -404,6 +404,20 @@ class BIM_PT_geometry(Panel): pass +class BIM_PT_tab_4D5D(Panel): + bl_label = "Costing and Scheduling" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + + def draw(self, context): + pass + + class BIM_PT_tab_structural(Panel): bl_label = "Structural" bl_space_type = "PROPERTIES" From dc36d74337e85b79b2330ceb9cdb9590cc951fbc Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:36:35 +0100 Subject: [PATCH 23/74] Hide Header for Costing and Schedule Tab --- src/blenderbim/blenderbim/bim/module/cost/ui.py | 1 - src/blenderbim/blenderbim/bim/module/resource/ui.py | 1 - src/blenderbim/blenderbim/bim/module/sequence/ui.py | 1 - src/blenderbim/blenderbim/bim/prop.py | 2 +- src/blenderbim/blenderbim/bim/ui.py | 1 + 5 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index c083331c76..d48621d940 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -26,7 +26,6 @@ from blenderbim.bim.module.cost.data import CostSchedulesData class BIM_PT_cost_schedules(Panel): bl_label = "Cost Schedules" bl_idname = "BIM_PT_cost_schedules" - bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 4ce36e12bf..090aa0f8c4 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -25,7 +25,6 @@ from blenderbim.bim.module.resource.data import ResourceData class BIM_PT_resources(Panel): bl_label = "Resources" bl_idname = "BIM_PT_resources" - bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index eb0ccc477d..82ee1e93ca 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -99,7 +99,6 @@ class BIM_PT_work_plans(Panel): class BIM_PT_work_schedules(Panel): bl_label = "Work Schedules" bl_idname = "BIM_PT_work_schedules" - bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 7dc0262881..34b587d58d 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -305,7 +305,7 @@ def get_tab(self, context): ("DRAWINGS", "Drawings and Documents", "", "DOCUMENTS", 3), ("SERVICES", "Services and Systems", "", "NETWORK_DRIVE", 4), ("STRUCTURE", "Structural Analysis", "", "EDITMODE_HLT", 5), - ("SCHEDULING", "Construction Scheduling", "", "NLA", 6), + ("SCHEDULING", "Costing and Scheduling", "", "NLA", 6), ("FM", "Facility Management", "", "PACKAGE", 7), ("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8), ("BLENDER", "Blender Properties", "", "BLENDER", 9), diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 81debd7574..b57deaa8b5 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -409,6 +409,7 @@ class BIM_PT_tab_4D5D(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): From fe01d252db798c139d744053a4eb848da764c410 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Thu, 10 Aug 2023 22:07:34 +0100 Subject: [PATCH 24/74] WIP - Scheduling UI improvements --- .../blenderbim/bim/module/cost/ui.py | 5 +- .../bim/module/sequence/__init__.py | 1 + .../blenderbim/bim/module/sequence/data.py | 18 -- .../bim/module/sequence/operator.py | 14 +- .../blenderbim/bim/module/sequence/prop.py | 32 +- .../blenderbim/bim/module/sequence/ui.py | 289 ++++++++++-------- src/blenderbim/blenderbim/core/sequence.py | 5 +- src/blenderbim/blenderbim/core/tool.py | 1 + src/blenderbim/blenderbim/tool/sequence.py | 15 +- .../api/sequence/add_work_schedule.py | 5 +- 10 files changed, 219 insertions(+), 166 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index d48621d940..832e69c871 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -47,8 +47,9 @@ class BIM_PT_cost_schedules(Panel): row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT") row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT") else: - row.label(text="No Cost Schedules Found found.", icon="COMMUNITY") - row = self.layout.row() + row.label(text="No Cost Schedules found.", icon="TEXT") + row = self.layout.row(align=True) + row.alignment = "RIGHT" row.prop(self.props, "cost_schedule_predefined_types") row.operator("bim.add_cost_schedule", icon="ADD", text="Add") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 8f6fbe4d6c..5eeff32eeb 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -137,6 +137,7 @@ classes = ( ui.BIM_PT_work_schedules, ui.BIM_PT_work_calendars, ui.BIM_PT_task_icom, + ui.BIM_PT_animation_tools, ui.BIM_UL_task_columns, ui.BIM_UL_task_inputs, ui.BIM_UL_task_resources, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/data.py b/src/blenderbim/blenderbim/bim/module/sequence/data.py index 123e51977c..f08a5d79ed 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/data.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/data.py @@ -19,7 +19,6 @@ import bpy import blenderbim.tool as tool import ifcopenshell -from ifcopenshell.util.doc import get_predefined_type_doc import ifcopenshell.util.date as dateutil @@ -37,7 +36,6 @@ class SequenceData: @classmethod def load(cls): cls.data = { - "predefined_types": cls.get_work_schedule_types(), "has_work_plans": cls.has_work_plans(), "has_work_schedules": cls.has_work_schedules(), "has_work_calendars": cls.has_work_calendars(), @@ -230,22 +228,6 @@ class SequenceData: data["NestingIndex"] = rel.RelatedObjects.index(task) cls.data["tasks"][task.id()] = data - @classmethod - def get_work_schedule_types(cls): - results = [] - declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule") - version = tool.Ifc.get_schema() - for attribute in declaration.attributes(): - if attribute.name() == "PredefinedType": - results.extend( - [ - (e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e)) - for e in attribute.type_of_attribute().declared_type().enumeration_items() - ] - ) - break - return results - class WorkScheduleData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 735430b403..99e5c11df8 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -129,9 +129,21 @@ class AddWorkSchedule(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_work_schedule" bl_label = "Add Work Schedule" bl_options = {"REGISTER", "UNDO"} + name: bpy.props.StringProperty() def _execute(self, context): - core.add_work_schedule(tool.Ifc) + core.add_work_schedule(tool.Ifc, tool.Sequence, name=self.name) + + def draw(self, context): + layout = self.layout + layout.prop(self, "name", text="Name") + self.props = context.scene.BIMWorkScheduleProperties + layout.prop(self.props, "work_schedule_predefined_types", text="Type") + if self.props.work_schedule_predefined_types == "USERDEFINED": + layout.prop(self.props,"object_type", text="Object type") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) class EditWorkSchedule(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 07929a4771..81fbf9fba1 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -20,6 +20,7 @@ import bpy import isodate import ifcopenshell.api import ifcopenshell.util.attribute +from ifcopenshell.util.doc import get_predefined_type_doc import blenderbim.tool as tool import blenderbim.core.sequence as core from blenderbim.bim.ifc import IfcStore @@ -224,10 +225,20 @@ def updateTaskDuration(self, context): def get_schedule_predefined_types(self, context): - if not SequenceData.is_loaded: - SequenceData.load() - return SequenceData.data["predefined_types"] - + results = [] + declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule") + version = tool.Ifc.get_schema() + for attribute in declaration.attributes(): + if attribute.name() == "PredefinedType": + results.extend( + [ + (e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e)) + for e in attribute.type_of_attribute().declared_type().enumeration_items() + if e != "BASELINE" + ] + ) + break + return results def update_visualisation_start(self, context): update_visualisation_start_finish(self, context, "visualisation_start") @@ -292,6 +303,14 @@ def update_filter_by_active_schedule(self, context): tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) ) +def switch_options(self, context): + if self.should_show_visualisation_ui: + self.should_show_snapshot_ui = False + +def switch_options2(self, context): + if self.should_show_snapshot_ui: + self.should_show_visualisation_ui = False + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) identification: StringProperty(name="Identification", update=updateTaskIdentification) @@ -352,6 +371,7 @@ class BIMWorkScheduleProperties(PropertyGroup): work_schedule_predefined_types: EnumProperty( items=get_schedule_predefined_types, name="Predefined Type", default=None ) + object_type: StringProperty(name="Object Type") durations_attributes: CollectionProperty(name="Durations Attributes", type=ISODuration) work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars") work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute) @@ -362,9 +382,9 @@ class BIMWorkScheduleProperties(PropertyGroup): active_task_index: IntProperty(name="Active Task Index", update=update_active_task_index) active_task_id: IntProperty(name="Active Task Id") task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) - should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False) + should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=True, update=switch_options) should_show_task_bar_selection: BoolProperty(name="Add to task bar", default=False) - should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False) + should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False, update=switch_options2) should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False) columns: CollectionProperty(name="Columns", type=Attribute) active_column_index: IntProperty(name="Active Column Index") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 82ee1e93ca..b945b72e4b 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -55,11 +55,10 @@ class BIM_PT_work_plans(Panel): def draw_work_plan_ui(self, work_plan): row = self.layout.row(align=True) row.label(text=work_plan["name"], icon="TEXT") - if self.props.active_work_plan_id == work_plan["id"]: if self.props.editing_type == "ATTRIBUTES": row.operator("bim.edit_work_plan", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_plan", text="", icon="CANCEL") + row.operator("bim.disable_editing_work_plan", text="Cancel", icon="CANCEL") elif self.props.active_work_plan_id: row.operator("bim.remove_work_plan", text="", icon="X").work_plan = work_plan["id"] else: @@ -116,10 +115,9 @@ class BIM_PT_work_schedules(Panel): WorkScheduleData.load() self.props = context.scene.BIMWorkScheduleProperties self.tprops = context.scene.BIMTaskTreeProperties - self.animation_props = context.scene.BIMAnimationProperties if not self.props.active_work_schedule_id: - row = self.layout.row() + row = self.layout.row(align=True) if SequenceData.data["has_work_schedules"]: row.label( text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]), @@ -127,25 +125,25 @@ class BIM_PT_work_schedules(Panel): ) else: row.label(text="No Work Schedules found.", icon="TEXT") - row = self.layout.row(align=True) - row.alignment = "RIGHT" - row.prop(self.props, "work_schedule_predefined_types") - row.operator("bim.add_work_schedule", text="Add new", icon="ADD") + row.operator("bim.add_work_schedule", text="Add", icon="ADD") for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items(): - self.draw_work_schedule_ui(work_schedule_id, work_schedule) def draw_work_schedule_ui(self, work_schedule_id, work_schedule): - if not work_schedule["PredefinedType"] == "BASELINE": + if work_schedule["PredefinedType"] == "BASELINE": + self.draw_readonly_work_schedule_ui(work_schedule_id) + else: row = self.layout.row(align=True) - row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") if self.props.active_work_schedule_id == work_schedule_id: + row.label( + text="Currently editing: {}[{}]".format(work_schedule["Name"], work_schedule["PredefinedType"]), + icon="LINENUMBERS_ON", + ) if self.props.editing_type == "WORK_SCHEDULE": - row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK") + row.operator("bim.edit_work_schedule", text="Apply", icon="CHECKMARK") elif self.props.editing_type == "TASKS": grid = self.layout.grid_flow(columns=2, even_columns=True) - col = grid.column() row1 = col.row(align=True) row1.alignment = "LEFT" @@ -180,33 +178,23 @@ class BIM_PT_work_schedules(Panel): row1.alignment = "RIGHT" row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY") row2 = col.row(align=True) - row2.prop( - self.props, "should_show_visualisation_ui", text="Animation Options", icon="CAMERA_STEREO" - ) - row2.prop(self.props, "should_show_snapshot_ui", text="Snapshot Options", icon="CAMERA_STEREO") - row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL") - else: + row.operator("bim.disable_editing_work_schedule", text="Cancel", icon="CANCEL") + if not self.props.active_work_schedule_id: + row.label(text="{}[{}]".format(work_schedule["Name"], work_schedule["PredefinedType"]) or "Unnamed", icon="LINENUMBERS_ON") row.operator( - "bim.enable_editing_work_schedule_tasks", text="", icon="ACTION" + "bim.enable_editing_work_schedule_tasks", text="Tasks", icon="ACTION" ).work_schedule = work_schedule_id row.operator( - "bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL" + "bim.enable_editing_work_schedule", text="Attributes", icon="GREASEPENCIL" ).work_schedule = work_schedule_id - row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id - + row.operator("bim.remove_work_schedule", text="Delete", icon="X").work_schedule = work_schedule_id if self.props.active_work_schedule_id == work_schedule_id: if self.props.editing_type == "WORK_SCHEDULE": self.draw_editable_work_schedule_ui() elif self.props.editing_type == "TASKS": self.draw_baseline_ui(work_schedule_id) self.draw_column_ui() - if self.props.should_show_visualisation_ui: - self.draw_visualisation_ui() - if self.props.should_show_snapshot_ui: - self.draw_snapshot_ui() self.draw_editable_task_ui(work_schedule_id) - else: - self.draw_readonly_work_schedule_ui(work_schedule_id) def draw_task_operators(self): row = self.layout.row(align=True) @@ -221,7 +209,7 @@ class BIM_PT_work_schedules(Panel): row.operator("bim.edit_task_time", text="", icon="CHECKMARK") elif self.props.editing_task_type == "ATTRIBUTES": row.operator("bim.edit_task", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_task", text="", icon="CANCEL") + row.operator("bim.disable_editing_task", text="Cancel", icon="CANCEL") else: row.prop(self.props, "show_task_operators", text="Edit", icon="GREASEPENCIL") if self.props.show_task_operators: @@ -271,103 +259,6 @@ class BIM_PT_work_schedules(Panel): self.layout.template_list("BIM_UL_task_columns", "", self.props, "columns", self.props, "active_column_index") - def draw_visualisation_ui(self): - row = self.layout.row(align=True) - row.label(text="Start Date/ Date Range:") - row = self.layout.row(align=True) - op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW") - op.target_prop = "BIMWorkScheduleProperties.visualisation_start" - op = row.operator("bim.datepicker", text=self.props.visualisation_finish or "Finish Date", icon="FF") - op.target_prop = "BIMWorkScheduleProperties.visualisation_finish" - op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH") - op.work_schedule = self.props.active_work_schedule_id - - row = self.layout.row(align=True) - row.label(text="Speed Settings") - row = self.layout.row(align=True) - row.prop(self.props, "speed_types", text="") - if self.props.speed_types == "FRAME_SPEED": - row.prop(self.props, "speed_animation_frames", text="") - row.prop(self.props, "speed_real_duration", text="") - elif self.props.speed_types == "DURATION_SPEED": - row.prop(self.props, "speed_animation_duration", text="") - row.prop(self.props, "speed_real_duration", text="") - elif self.props.speed_types == "MULTIPLIER_SPEED": - row.prop(self.props, "speed_multiplier", text="") - row = self.layout.row(align=True) - row.label(text="Display Settings") - row = self.layout.row(align=True) - if not self.animation_props.is_editing: - op = row.operator( - "bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04" - ) - else: - op = row.operator( - "bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01" - ) - - row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN") - if self.animation_props.should_show_task_bar_options: - row = self.layout.row() - row.label(text="Task Bar Options", icon="NLA_PUSHDOWN") - row.alignment = "LEFT" - row = self.layout.row(align=True) - row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN") - row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN") - - grid = self.layout.grid_flow(columns=2, even_columns=True) - # Column1 - col = grid.column() - - row = col.row(align=True) - row.prop(self.animation_props, "color_progress") - - row = col.row(align=True) - row.prop(self.animation_props, "color_full") - - if self.animation_props.is_editing: - self.draw_visualisation_settings_ui() - - row = self.layout.row(align=True) - op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA") - op.work_schedule = self.props.active_work_schedule_id - - def draw_snapshot_ui(self): - row = self.layout.row(align=True) - row.label(text="Create Construction Snapshot:") - row = self.layout.row(align=True) - op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="REW") - op.target_prop = "BIMWorkScheduleProperties.visualisation_start" - op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="RESTRICT_RENDER_OFF") - op.work_schedule = self.props.active_work_schedule_id - - def draw_visualisation_settings_ui(self): - grid = self.layout.grid_flow(columns=2, even_columns=True) - col = grid.column() - row1 = col.row(align=True) - row1.label(text="INPUT COLORS", icon="COLLECTION_COLOR_01") - row1 = col.row() - row1.template_list( - "BIM_UL_animation_colors", - "", - self.animation_props, - "task_colors_components_inputs", - self.animation_props, - "active_color_component_inputs_index", - ) - col = grid.column() - row1 = col.row(align=True) - row1.label(text="OUTPUT COLORS", icon="COLLECTION_COLOR_04") - row1 = col.row() - row1.template_list( - "BIM_UL_animation_colors", - "", - self.animation_props, - "task_colors_components_outputs", - self.animation_props, - "active_color_component_outputs_index", - ) - def draw_editable_work_schedule_ui(self): draw_attributes(self.props.work_schedule_attributes, self.layout) @@ -424,12 +315,12 @@ class BIM_PT_work_schedules(Panel): if self.props.active_sequence_id == sequence["id"]: if self.props.editing_sequence_type == "ATTRIBUTES": row.operator("bim.edit_sequence_attributes", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_sequence", text="", icon="CANCEL") + row.operator("bim.disable_editing_sequence", text="Cancel", icon="CANCEL") self.draw_editable_sequence_attributes_ui() elif self.props.editing_sequence_type == "LAG_TIME": op = row.operator("bim.edit_sequence_lag_time", text="", icon="CHECKMARK") op.lag_time = sequence["TimeLag"] - row.operator("bim.disable_editing_sequence", text="", icon="CANCEL") + row.operator("bim.disable_editing_sequence", text="Cancel", icon="CANCEL") self.draw_editable_sequence_lag_time_ui() else: if sequence["TimeLag"]: @@ -494,7 +385,7 @@ class BIM_PT_work_schedules(Panel): "id" ] baseline_row.operator( - "bim.enable_editing_work_schedule_tasks", text="", icon="ACTION" + "bim.enable_editing_work_schedule_tasks", text="Display Schedule", icon="ACTION" ).work_schedule = baseline["id"] baseline_row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = baseline["id"] @@ -524,6 +415,140 @@ class BIM_PT_work_schedules(Panel): ) +class BIM_PT_animation_tools(Panel): + bl_label = "Animation Tools" + bl_idname = "BIM_PT_animation_tools" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_work_schedules" + + @classmethod + def poll(cls, context): + props = context.scene.BIMWorkScheduleProperties + if props.active_work_schedule_id: + return True + return False + + def draw(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.animation_props = context.scene.BIMAnimationProperties + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.prop( + self.props, "should_show_visualisation_ui", text="Animation Settings", icon="SETTINGS" + ) + row.prop(self.props, "should_show_snapshot_ui", text="Snapshot Settings", icon="SETTINGS") + if self.props.should_show_visualisation_ui: + self.draw_visualisation_ui() + if self.props.should_show_snapshot_ui: + self.draw_snapshot_ui() + + def draw_visualisation_ui(self): + row = self.layout.row(align=True) + row.label(text="Start Date/ Date Range:", icon="CAMERA_DATA") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Start Date", icon="REW") + op.target_prop = "BIMWorkScheduleProperties.visualisation_start" + op = row.operator("bim.datepicker", text=self.props.visualisation_finish or "Finish Date", icon="FF") + op.target_prop = "BIMWorkScheduleProperties.visualisation_finish" + op = row.operator("bim.guess_date_range", text="Guess", icon="FILE_REFRESH") + op.work_schedule = self.props.active_work_schedule_id + + row = self.layout.row(align=True) + row.label(text="Speed Settings") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.prop(self.props, "speed_types", text="") + if self.props.speed_types == "FRAME_SPEED": + row.prop(self.props, "speed_animation_frames", text="") + row.prop(self.props, "speed_real_duration", text="") + elif self.props.speed_types == "DURATION_SPEED": + row.prop(self.props, "speed_animation_duration", text="") + row.prop(self.props, "speed_real_duration", text="") + elif self.props.speed_types == "MULTIPLIER_SPEED": + row.prop(self.props, "speed_multiplier", text="") + row = self.layout.row(align=True) + row.label(text="Display Settings") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + if not self.animation_props.is_editing: + op = row.operator( + "bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04" + ) + else: + op = row.operator( + "bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01" + ) + + row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN") + if self.animation_props.should_show_task_bar_options: + row = self.layout.row() + row.label(text="Task Bar Options", icon="NLA_PUSHDOWN") + row.alignment = "LEFT" + row = self.layout.row(align=True) + row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN") + row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN") + + grid = self.layout.grid_flow(columns=2, even_columns=True) + # Column1 + col = grid.column() + + row = col.row(align=True) + row.prop(self.animation_props, "color_progress") + + row = col.row(align=True) + row.prop(self.animation_props, "color_full") + + if self.animation_props.is_editing: + self.draw_visualisation_settings_ui() + + row = self.layout.row(align=True) + row.alignment = "RIGHT" + op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA") + op.work_schedule = self.props.active_work_schedule_id + + def draw_snapshot_ui(self): + row = self.layout.row(align=True) + row.label(text="Date of Snapshot:", icon="CAMERA_STEREO") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="PROP_PROJECTED") + op.target_prop = "BIMWorkScheduleProperties.visualisation_start" + row = self.layout.row(align=True) + row.alignment = "RIGHT" + op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="CAMERA_STEREO") + op.work_schedule = self.props.active_work_schedule_id + + def draw_visualisation_settings_ui(self): + grid = self.layout.grid_flow(columns=2, even_columns=True) + col = grid.column() + row1 = col.row(align=True) + row1.label(text="INPUT COLORS", icon="COLLECTION_COLOR_01") + row1 = col.row() + row1.template_list( + "BIM_UL_animation_colors", + "", + self.animation_props, + "task_colors_components_inputs", + self.animation_props, + "active_color_component_inputs_index", + ) + col = grid.column() + row1 = col.row(align=True) + row1.label(text="OUTPUT COLORS", icon="COLLECTION_COLOR_04") + row1 = col.row() + row1.template_list( + "BIM_UL_animation_colors", + "", + self.animation_props, + "task_colors_components_outputs", + self.animation_props, + "active_color_component_outputs_index", + ) + class BIM_PT_task_icom(Panel): bl_label = "Task ICOM" bl_idname = "BIM_PT_task_icom" @@ -846,7 +871,7 @@ class BIM_PT_work_calendars(Panel): if self.props.active_work_calendar_id == work_calendar_id: if self.props.editing_type == "ATTRIBUTES": row.operator("bim.edit_work_calendar", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_calendar", text="", icon="CANCEL") + row.operator("bim.disable_editing_work_calendar", text="Cancel", icon="CANCEL") elif self.props.active_work_calendar_id: row.operator("bim.remove_work_calendar", text="", icon="X").work_calendar = work_calendar_id else: @@ -884,7 +909,7 @@ class BIM_PT_work_calendars(Panel): row.label(text="{} - {}".format(work_time["Start"] or "*", work_time["Finish"] or "*")) if self.props.active_work_time_id == work_time["id"]: row.operator("bim.edit_work_time", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_work_time", text="", icon="CANCEL") + row.operator("bim.disable_editing_work_time", text="Cancel", icon="CANCEL") elif self.props.active_work_time_id: op = row.operator("bim.remove_work_time", text="", icon="X") op.work_time = work_time["id"] diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index 1a68ffd9ea..5d8bb01936 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -50,8 +50,9 @@ def enable_editing_work_plan_schedules(sequence, work_plan=None): sequence.enable_editing_work_plan_schedules(work_plan) -def add_work_schedule(ifc): - return ifc.run("sequence.add_work_schedule") +def add_work_schedule(ifc, sequence, name=None): + predefined_type, object_type = sequence.get_user_predefined_type() + return ifc.run("sequence.add_work_schedule", name=name, predefined_type=predefined_type, object_type=object_type) def remove_work_schedule(ifc, work_schedule=None): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 44b14a3e3c..9d644f1a81 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -719,6 +719,7 @@ class Sequence: def get_task_time_attributes(cls): pass def get_task_time(cls, task): pass def get_tasks_for_product(cls, product, work_schedule): pass + def get_user_predefined_type(cls): pass def get_work_calendar_attributes(cls): pass def get_work_plan_attributes(cls): pass def get_work_schedule_attributes(cls): pass diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index eef330f713..39fc55cc99 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -811,10 +811,9 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def update_visualisation_date(cls, start_date, finish_date): def canonicalise_time(time): - if not time: - return "-" return time.strftime("%d/%m/%y") - + if not (start_date and finish_date): + return props = bpy.context.scene.BIMWorkScheduleProperties props.visualisation_start = canonicalise_time(start_date) props.visualisation_finish = canonicalise_time(finish_date) @@ -1607,4 +1606,12 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def is_sort_reversed(cls): - return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed \ No newline at end of file + return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed + + @classmethod + def get_user_predefined_type(cls): + predefined_type = bpy.context.scene.BIMWorkScheduleProperties.work_schedule_predefined_types + object_type = None + if predefined_type == "USERDEFINED": + object_type = bpy.context.scene.BIMWorkScheduleProperties.object_type + return predefined_type, object_type \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py index ec63d53b3d..a9c775d338 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py @@ -27,6 +27,7 @@ class Usecase: file, name="Unnamed", predefined_type="NOTDEFINED", + object_type=None, start_time=None, work_plan=None, ): @@ -77,6 +78,7 @@ class Usecase: self.settings = { "name": name, "predefined_type": predefined_type, + "object_type": object_type, "start_time": start_time or datetime.now(), "work_plan": work_plan, } @@ -98,7 +100,8 @@ class Usecase: work_schedule.StartTime = ifcopenshell.util.date.datetime2ifc( self.settings["start_time"], "IfcDateTime" ) - + if self.settings["object_type"]: + work_schedule.ObjectType = self.settings["object_type"] if self.settings["work_plan"]: ifcopenshell.api.run( "aggregate.assign_object", From 268fed761e82a585551a18ded81afc905a6e1992 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Fri, 11 Aug 2023 02:03:07 +0100 Subject: [PATCH 25/74] simplify updateTaskDuration --- src/blenderbim/blenderbim/bim/module/sequence/prop.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 81fbf9fba1..ef4eff3de4 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -209,17 +209,12 @@ def updateTaskDuration(self, context): self.duration = "-" return - self.file = tool.Ifc.get() - task = self.file.by_id(self.ifc_definition_id) + task = tool.Ifc.get().by_id(self.ifc_definition_id) if task.TaskTime: task_time = task.TaskTime else: - task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=task) - ifcopenshell.api.run( - "sequence.edit_task_time", - self.file, - **{"task_time": task_time, "attributes": {"ScheduleDuration": duration}}, - ) + task_time = tool.Ifc.run("sequence.add_task_time", task=task) + tool.Ifc.run("sequence.edit_task_time", task_time=task_time, attributes={"ScheduleDuration": duration}) SequenceData.load() bpy.ops.bim.load_task_properties() From 0f5c82dffd5852d5714b0dce947739d07c257224 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Fri, 11 Aug 2023 02:05:37 +0100 Subject: [PATCH 26/74] animation settings now use human readable durations to make it less confusing --- .../blenderbim/bim/module/sequence/prop.py | 4 ++-- src/blenderbim/blenderbim/tool/sequence.py | 10 ++++++---- src/blenderbim/test/bim/feature/sequence.feature | 12 ++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index ef4eff3de4..28a77506b4 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -412,9 +412,9 @@ class BIMWorkScheduleProperties(PropertyGroup): visualisation_start: StringProperty(name="Visualisation Start", update=update_visualisation_start) visualisation_finish: StringProperty(name="Visualisation Finish", update=update_visualisation_finish) speed_multiplier: FloatProperty(name="Speed Multiplier", default=10000) - speed_animation_duration: StringProperty(name="Speed Animation Duration", default="PT1S") + speed_animation_duration: StringProperty(name="Speed Animation Duration", default="1 s") speed_animation_frames: IntProperty(name="Speed Animation Frames", default=24) - speed_real_duration: StringProperty(name="Speed Real Duration", default="P1W") + speed_real_duration: StringProperty(name="Speed Real Duration", default="1 w") speed_types: EnumProperty( items=[ ("FRAME_SPEED", "Frame-based", "e.g. 25 frames = 1 real week"), diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 39fc55cc99..157f2a6df7 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -1226,21 +1226,23 @@ class Sequence(blenderbim.core.tool.Sequence): start, finish, props.speed_animation_frames, - isodate.parse_duration(props.speed_real_duration), + ifcopenshell.util.date.parse_duration(props.speed_real_duration) ) elif props.speed_types == "DURATION_SPEED": + animation_duration = ifcopenshell.util.date.parse_duration(props.speed_animation_duration) + real_duration = ifcopenshell.util.date.parse_duration(props.speed_real_duration) return calculate_using_duration( start, finish, fps, - isodate.parse_duration(props.speed_animation_duration), - isodate.parse_duration(props.speed_real_duration), + animation_duration, + real_duration, ) elif props.speed_types == "MULTIPLIER_SPEED": return calculate_using_multiplier( start, finish, - fps, + 1, props.speed_multiplier, ) diff --git a/src/blenderbim/test/bim/feature/sequence.feature b/src/blenderbim/test/bim/feature/sequence.feature index 1015ef660e..81e40d63fd 100644 --- a/src/blenderbim/test/bim/feature/sequence.feature +++ b/src/blenderbim/test/bim/feature/sequence.feature @@ -326,7 +326,7 @@ Scenario: See the current frame date as text And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then the object "Timeline" has a body of "2021-01-01" @@ -357,7 +357,7 @@ Scenario: Animate the construction of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "True" @@ -393,7 +393,7 @@ Scenario: Animate the demolition of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "False" @@ -432,7 +432,7 @@ Scenario: Animate the operation of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]" @@ -472,7 +472,7 @@ Scenario: Animate the movement of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/FromObject').color[:]" is "[1.0, 1.0, 1.0, 1]" @@ -516,7 +516,7 @@ Scenario: Animate the consumption of a wall And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" - And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "P1W" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" When I am on frame "1" Then "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]" From a374ee39d36b2a6e5ce277034e40c8a2d1878030 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Fri, 11 Aug 2023 02:06:49 +0100 Subject: [PATCH 27/74] simplify setup_default_task_columns --- .../blenderbim/bim/module/sequence/ui.py | 2 +- src/blenderbim/blenderbim/tool/sequence.py | 20 +++---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index b945b72e4b..43cbb3bf70 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -233,7 +233,7 @@ class BIM_PT_work_schedules(Panel): if not self.props.should_show_column_ui: return row = self.layout.row() - row.operator("bim.setup_default_task_columns", text="Add Default Columns", icon="ANCHOR_BOTTOM") + row.operator("bim.setup_default_task_columns", text="Setup Default Columns", icon="ANCHOR_BOTTOM") row.alignment = "RIGHT" row = self.layout.row(align=True) row.prop(self.props, "column_types", text="") diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 157f2a6df7..6c62dd2c5f 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -719,26 +719,12 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def setup_default_task_columns(cls): - items = [ - { - "column_type": "IfcTaskTime", - "name": "ScheduleStart", - }, - { - "column_type": "IfcTaskTime", - "name": "ScheduleFinish", - }, - { - "column_type": "IfcTaskTime", - "name": "ScheduleDuration", - }, - ] - props = bpy.context.scene.BIMWorkScheduleProperties props.columns.clear() - for item in items: + default_columns = ["ScheduleStart","ScheduleFinish","ScheduleDuration"] + for item in default_columns: new = props.columns.add() - new.name = f"{item['column_type']}.{item['name']}" + new.name = f"IfcTaskTime.{item}" new.data_type = "string" @classmethod From a2b6f21c7a08621b4a321b5e12fdc81cd37e9aff Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Aug 2023 16:01:01 +1000 Subject: [PATCH 28/74] See #2894. Implement SVG mirroring options (typically used for RCPs or useful when converting to other things like DXF). --- src/serializers/SvgSerializer.cpp | 25 +++++++++++++++++++++++-- src/serializers/SvgSerializer.h | 20 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index 43e2c8b941..f1962e6570 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -761,7 +761,14 @@ void SvgSerializer::write(const geometry_data& data) { // SVG has a coordinate system with the origin in the *upper*-left corner // therefore we mirror the shape along the XZ-plane. gp_Trsf trsf_mirror; - trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + if (!mirror_y_) { + trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + } + if (mirror_x_) { + gp_Trsf mirror_x; + mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX())); + trsf_mirror.PreMultiply(mirror_x); + } BRepBuilderAPI_Transform make_transform_mirror(compound_unmirrored, trsf_mirror, true); make_transform_mirror.Build(); // (When determinant < 0, copy is implied and the input is not mutated.) @@ -1667,6 +1674,13 @@ std::array, 3> SvgSerializer::resize() { cy = ymin * sc; } + if (mirror_y_) { + cy = - size_->second - cy; + } + if (mirror_x_) { + cx = - size_->first - cx; + } + m = {{ {{sc,0,-cx}},{{0,sc,-cy}},{{0,0,1}} }}; float_item_list::const_iterator it; @@ -1704,7 +1718,14 @@ void SvgSerializer::draw_hlr(const gp_Pln& pln, const drawing_key& drawing_name) TopoDS_Shape hlr_compound; if (drawing_name.first == nullptr) { gp_Trsf trsf_mirror; - trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + if (!mirror_y_) { + trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + } + if (mirror_x_) { + gp_Trsf mirror_x; + mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX())); + trsf_mirror.PreMultiply(mirror_x); + } BRepBuilderAPI_Transform make_transform_mirror(hlr_compound_unmirrored, trsf_mirror, true); make_transform_mirror.Build(); hlr_compound = make_transform_mirror.Shape(); diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index 19ade12ee6..6cd806737c 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -521,6 +521,8 @@ protected: bool emit_building_storeys_; bool no_css_; bool unify_inputs_; + bool mirror_y_; + bool mirror_x_; int profile_threshold_; @@ -572,6 +574,8 @@ public: , polygonal_(false) , emit_building_storeys_(true) , no_css_(false) + , mirror_y_(false) + , mirror_x_(false) , unify_inputs_(false) , profile_threshold_(-1) , file(0) @@ -713,6 +717,22 @@ public: return profile_threshold_; } + void setMirrorY(bool b) { + mirror_y_ = b; + } + + bool getMirrorY() const { + return mirror_y_; + } + + void setMirrorX(bool b) { + mirror_x_ = b; + } + + bool getMirrorX() const { + return mirror_x_; + } + protected: std::string writeMetadata(const drawing_meta& m); }; From 000116bb3d6d44129ec385efb2eb20d02a0d5aec Mon Sep 17 00:00:00 2001 From: Kristoffer Andersen Date: Fri, 11 Aug 2023 08:22:52 +0200 Subject: [PATCH 29/74] Update build.sh limit number of schema variants --- conda/build.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conda/build.sh b/conda/build.sh index adeeb58d95..275e2cbfc5 100644 --- a/conda/build.sh +++ b/conda/build.sh @@ -12,9 +12,8 @@ if [ `uname` == Darwin ]; then export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress" fi -export SCHEMA_VERSIONS="2x3;4;4x3;4x3_add1" - cmake -G Ninja \ + -DSCHEMA_VERSIONS="2x3;4;4x1;4x3;4x3_add1" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=$PREFIX \ ${CMAKE_PLATFORM_FLAGS[@]} \ From 4c8007dcfe9e80a2f28f18f6432bf565b07bd44b Mon Sep 17 00:00:00 2001 From: Kristoffer Andersen Date: Fri, 11 Aug 2023 08:44:25 +0200 Subject: [PATCH 30/74] Update meta.yaml pin compatible boost, occt and cgal-cpp --- conda/meta.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conda/meta.yaml b/conda/meta.yaml index 38c4d2bbc5..5afdd5a56c 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -36,10 +36,10 @@ requirements: run: - python - - boost-cpp - - occt ==7.7.0 + - {{ pin_compatible('occt', max_pin='x.x.x') }} + - {{ pin_compatible('cgal-cpp', max_pin='x.x.x') }} + - {{ pin_compatible('boost-cpp', max_pin='x.x.x') }} - libxml2 - - cgal-cpp - hdf5 - mpfr - gmp # [unix] From 4deb1836add21b1c33f287c8eb10cbcd1b9e2ef2 Mon Sep 17 00:00:00 2001 From: Kristoffer Andersen Date: Fri, 11 Aug 2023 11:42:08 +0200 Subject: [PATCH 31/74] Update bld.bat reduce number of schema variations on windows as well --- conda/bld.bat | 1 + 1 file changed, 1 insertion(+) diff --git a/conda/bld.bat b/conda/bld.bat index b25f46c9d6..c426d75916 100644 --- a/conda/bld.bat +++ b/conda/bld.bat @@ -4,6 +4,7 @@ set MY_PY_VER=%PY_VER:.=% set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib" cmake -G "Ninja" ^ + -D SCHEMA_VERSIONS="2x3;4;4x1;4x3;4x3_add1" ^ -D CMAKE_BUILD_TYPE:STRING=Release ^ -D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^ -D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^ From 6eea1e9035bb101ace185b618a1f85076e589e4d Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:36:14 +0100 Subject: [PATCH 32/74] feature to clear previous 4D Animation (object keyframes & colors) --- .../bim/module/sequence/__init__.py | 27 +++++++------- .../bim/module/sequence/operator.py | 9 +++++ .../blenderbim/bim/module/sequence/ui.py | 14 ++++++-- src/blenderbim/blenderbim/core/sequence.py | 4 +++ src/blenderbim/blenderbim/tool/sequence.py | 33 +++++++++++------ .../test/bim/feature/sequence.feature | 36 ++++++++++++++++++- 6 files changed, 96 insertions(+), 27 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 5eeff32eeb..2a43a798f4 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -20,12 +20,10 @@ import bpy from . import ui, prop, operator classes = ( - operator.ExpandAllTasks, - operator.ContractAllTasks, operator.AddSummaryTask, operator.AddTask, - operator.AddTaskColumn, operator.AddTaskBars, + operator.AddTaskColumn, operator.AddTimePeriod, operator.AddWorkCalendar, operator.AddWorkPlan, @@ -42,11 +40,15 @@ classes = ( operator.BlenderBIM_DatePickerSetDate, operator.BlenderBIM_RedrawDatePicker, operator.CalculateTaskDuration, + operator.ClearPreviousAnimation, + operator.ContractAllTasks, operator.ContractTask, - operator.CopyTaskAttribute, operator.CopyTask, + operator.CopyTaskAttribute, + operator.CreateBaseline, operator.DisableEditingSequence, operator.DisableEditingTask, + operator.DisableEditingTaskAnimationColors, operator.DisableEditingTaskTime, operator.DisableEditingWorkCalendar, operator.DisableEditingWorkPlan, @@ -67,23 +69,27 @@ classes = ( operator.EnableEditingTaskCalendar, operator.EnableEditingTaskSequence, operator.EnableEditingTaskTime, - operator.EnableEditingWorkScheduleTasks, operator.EnableEditingWorkCalendar, operator.EnableEditingWorkCalendarTimes, operator.EnableEditingWorkPlan, operator.EnableEditingWorkPlanSchedules, operator.EnableEditingWorkSchedule, + operator.EnableEditingWorkScheduleTasks, operator.EnableEditingWorkTime, + operator.ExpandAllTasks, operator.ExpandTask, operator.ExportMSP, operator.ExportP6, operator.GenerateGanttChart, operator.GuessDateRange, - operator.ImportMSP, + operator.HighlightTask, operator.ImportCSV, + operator.ImportMSP, operator.ImportP6, operator.ImportP6XER, operator.ImportPP, + operator.LoadProductTasks, + operator.LoadTaskAnimationColors, operator.LoadTaskInputs, operator.LoadTaskOutputs, operator.LoadTaskProperties, @@ -98,10 +104,10 @@ classes = ( operator.RemoveWorkSchedule, operator.RemoveWorkTime, operator.ReorderTask, - operator.SelectTaskRelatedProducts, operator.SelectTaskRelatedInputs, - operator.SelectWorkScheduleProducts, + operator.SelectTaskRelatedProducts, operator.SelectUnassignedWorkScheduleProducts, + operator.SelectWorkScheduleProducts, operator.SetTaskSortColumn, operator.SetupDefaultTaskColumns, operator.UnassignLagTime, @@ -113,11 +119,6 @@ classes = ( operator.UnassignWorkSchedule, operator.VisualiseWorkScheduleDate, operator.VisualiseWorkScheduleDateRange, - operator.LoadTaskAnimationColors, - operator.DisableEditingTaskAnimationColors, - operator.LoadProductTasks, - operator.HighlightTask, - operator.CreateBaseline, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 99e5c11df8..49d12f462c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1401,3 +1401,12 @@ class CreateBaseline(bpy.types.Operator, tool.Ifc.Operator): def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) + + +class ClearPreviousAnimation(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.clear_previous_animation" + bl_label = "Clear Previous Animation" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.clear_previous_animation(tool.Sequence) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 43cbb3bf70..fa04f9502c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -444,6 +444,16 @@ class BIM_PT_animation_tools(Panel): self.draw_visualisation_ui() if self.props.should_show_snapshot_ui: self.draw_snapshot_ui() + self.draw_processing_options() + + + def draw_processing_options(self): + row = self.layout.row(align=True) + row.alignment = "LEFT" + row.label(text="Processing Tools") + row = self.layout.row() + row.alignment = "RIGHT" + row.operator("bim.clear_previous_animation", text="Clear Previous Animation", icon="TRACKING_CLEAR_FORWARDS") def draw_visualisation_ui(self): row = self.layout.row(align=True) @@ -467,12 +477,13 @@ class BIM_PT_animation_tools(Panel): row.prop(self.props, "speed_real_duration", text="") elif self.props.speed_types == "DURATION_SPEED": row.prop(self.props, "speed_animation_duration", text="") + row.label(text="->") row.prop(self.props, "speed_real_duration", text="") elif self.props.speed_types == "MULTIPLIER_SPEED": row.prop(self.props, "speed_multiplier", text="") row = self.layout.row(align=True) row.label(text="Display Settings") - row = self.layout.row(align=True) + row = self.layout.row() row.alignment = "RIGHT" if not self.animation_props.is_editing: op = row.operator( @@ -506,7 +517,6 @@ class BIM_PT_animation_tools(Panel): self.draw_visualisation_settings_ui() row = self.layout.row(align=True) - row.alignment = "RIGHT" op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA") op.work_schedule = self.props.active_work_schedule_id diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index 5d8bb01936..acab879966 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -530,6 +530,7 @@ def disable_editing_task_animation_colors(sequence): def visualise_work_schedule_date_range(sequence, work_schedule=None): + sequence.clear_objects_animation(include_blender_objects=False) settings = sequence.get_animation_settings() if settings: product_frames = sequence.get_animation_product_frames(work_schedule, settings) @@ -580,3 +581,6 @@ def reorder_task_nesting(ifc, sequence, task, new_index): def create_baseline(ifc, sequence, work_schedule, name): ifc.run("sequence.create_baseline", work_schedule=work_schedule, name=name) + +def clear_previous_animation(sequence): + sequence.clear_objects_animation(include_blender_objects=False) diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 6c62dd2c5f..5cb93acdc7 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -19,7 +19,6 @@ import bpy import re import os -import isodate import ifcopenshell import ifcopenshell.util.sequence import ifcopenshell.util.date @@ -36,7 +35,6 @@ from datetime import datetime import mathutils import pystache import webbrowser -from datetime import timedelta class Sequence(blenderbim.core.tool.Sequence): @@ -212,7 +210,9 @@ class Sequence(blenderbim.core.tool.Sequence): item.calendar = "" item.derived_calendar = calendar.Name or "Unnamed" if calendar else "" - if task.TaskTime and (task.TaskTime.ScheduleStart or task.TaskTime.ScheduleFinish or task.TaskTime.ScheduleDuration): + if task.TaskTime and ( + task.TaskTime.ScheduleStart or task.TaskTime.ScheduleFinish or task.TaskTime.ScheduleDuration + ): task_time = task.TaskTime item.start = ( canonicalise_time(ifcopenshell.util.date.ifc2datetime(task_time.ScheduleStart)) @@ -721,7 +721,7 @@ class Sequence(blenderbim.core.tool.Sequence): def setup_default_task_columns(cls): props = bpy.context.scene.BIMWorkScheduleProperties props.columns.clear() - default_columns = ["ScheduleStart","ScheduleFinish","ScheduleDuration"] + default_columns = ["ScheduleStart", "ScheduleFinish", "ScheduleDuration"] for item in default_columns: new = props.columns.add() new.name = f"IfcTaskTime.{item}" @@ -798,6 +798,7 @@ class Sequence(blenderbim.core.tool.Sequence): def update_visualisation_date(cls, start_date, finish_date): def canonicalise_time(time): return time.strftime("%d/%m/%y") + if not (start_date and finish_date): return props = bpy.context.scene.BIMWorkScheduleProperties @@ -1212,7 +1213,7 @@ class Sequence(blenderbim.core.tool.Sequence): start, finish, props.speed_animation_frames, - ifcopenshell.util.date.parse_duration(props.speed_real_duration) + ifcopenshell.util.date.parse_duration(props.speed_real_duration), ) elif props.speed_types == "DURATION_SPEED": animation_duration = ifcopenshell.util.date.parse_duration(props.speed_animation_duration) @@ -1299,15 +1300,24 @@ class Sequence(blenderbim.core.tool.Sequence): if obj.animation_data: obj.animation_data_clear() + @classmethod + def clear_object_color(cls, obj): + obj.color = (1.0, 1.0, 1.0, 1.0) + + @classmethod + def display_object(cls, obj): + if not obj.visible_get(): + obj.hide_viewport = False + obj.hide_render = False + @classmethod def clear_objects_animation(cls, include_blender_objects=True): for obj in bpy.data.objects: if not include_blender_objects and not obj.BIMObjectProperties.ifc_definition_id: continue cls.clear_object_animation(obj) - if not obj.visible_get(): - obj.hide_viewport = False - obj.hide_render = False + cls.clear_object_color(obj) + cls.display_object(obj) @classmethod def animate_objects(cls, settings, frames, clear_previous=True, animation_type=""): @@ -1502,13 +1512,13 @@ class Sequence(blenderbim.core.tool.Sequence): compare_start = schedule_start compare_finish = schedule_finish task_name = task.Name or "Unnamed" - task_name = task_name.replace('\n', "") + task_name = task_name.replace("\n", "") data = { "pID": task.id(), "pName": task_name, "pCaption": task_name, "pStart": schedule_start, - "pEnd": schedule_finish , + "pEnd": schedule_finish, "pPlanStart": compare_start, "pPlanEnd": compare_finish, "pMile": 1 if task.IsMilestone else 0, @@ -1602,4 +1612,5 @@ class Sequence(blenderbim.core.tool.Sequence): object_type = None if predefined_type == "USERDEFINED": object_type = bpy.context.scene.BIMWorkScheduleProperties.object_type - return predefined_type, object_type \ No newline at end of file + return predefined_type, object_type + diff --git a/src/blenderbim/test/bim/feature/sequence.feature b/src/blenderbim/test/bim/feature/sequence.feature index 81e40d63fd..dc5ddd29af 100644 --- a/src/blenderbim/test/bim/feature/sequence.feature +++ b/src/blenderbim/test/bim/feature/sequence.feature @@ -532,6 +532,39 @@ Scenario: Animate the consumption of a wall Then "scene.objects.get('IfcWall/Cube').hide_render" is "True" + +Scenario: Clear Previous Animation + Given an empty IFC project + And I press "bim.add_work_schedule" + And the variable "work_schedule" is "IfcStore.get_file().by_type('IfcWorkSchedule')[0].id()" + And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})" + And I press "bim.add_summary_task(work_schedule={work_schedule})" + And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()" + And I press "bim.enable_editing_task_attributes(task={task})" + And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION" + And I press "bim.edit_task" + And I press "bim.enable_editing_task_time(task={task})" + And I set "scene.BIMWorkScheduleProperties.task_time_attributes.get('ScheduleStart').string_value" to "2021-01-02" + And I set "scene.BIMWorkScheduleProperties.task_time_attributes.get('ScheduleFinish').string_value" to "2021-01-06" + And I press "bim.edit_task_time" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + And I press "bim.assign_product(task={task})" + And I set "scene.BIMWorkScheduleProperties.visualisation_start" to "01/01/21" + And I set "scene.BIMWorkScheduleProperties.visualisation_finish" to "01/02/21" + And I set "scene.BIMWorkScheduleProperties.speed_types" to "FRAME_SPEED" + And I set "scene.BIMWorkScheduleProperties.speed_animation_frames" to "7" + And I set "scene.BIMWorkScheduleProperties.speed_real_duration" to "1 w" + And I press "bim.visualise_work_schedule_date_range(work_schedule={work_schedule})" + And I press "bim.clear_previous_animation" + When I am on frame "3" + Then "scene.objects.get('IfcWall/Cube').hide_viewport" is "False" + And "scene.objects.get('IfcWall/Cube').hide_render" is "False" + And "scene.objects.get('IfcWall/Cube').color[:]" is "[1.0, 1.0, 1.0, 1]" + Scenario: Generate Gantt Chart Given an empty IFC project And I press "bim.add_work_schedule" @@ -808,4 +841,5 @@ Scenario: Duplicate Task and edit sequence Relationship And I press "bim.assign_successor(task={nested_task_two})" And I press "bim.duplicate_task(task={task})" When I press "bim.enable_editing_task_sequence(task={nested_task_one})" - Then nothing happens \ No newline at end of file + Then nothing happens + From 70bea0c8657ed250d9c892e3fdb87bcd8f19a689 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:39:58 +0100 Subject: [PATCH 33/74] Feature to add a camera to extents of the scene objects --- .../blenderbim/bim/module/sequence/__init__.py | 1 + .../blenderbim/bim/module/sequence/operator.py | 10 ++++++++++ .../blenderbim/bim/module/sequence/ui.py | 1 + src/blenderbim/blenderbim/core/sequence.py | 3 +++ src/blenderbim/blenderbim/core/tool.py | 3 +++ src/blenderbim/blenderbim/tool/sequence.py | 15 +++++++++++++++ src/blenderbim/test/bim/feature/sequence.feature | 8 ++++++++ 7 files changed, 41 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 2a43a798f4..65d1fb887e 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -20,6 +20,7 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddAnimationCamera, operator.AddSummaryTask, operator.AddTask, operator.AddTaskBars, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 49d12f462c..bb6f9d17ab 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1410,3 +1410,13 @@ class ClearPreviousAnimation(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.clear_previous_animation(tool.Sequence) + + +class AddAnimationCamera(bpy.types.Operator): + bl_idname = "bim.add_animation_camera" + bl_label = "Add Camera to Scene" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + core.add_animation_camera(tool.Sequence) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index fa04f9502c..64ed3399f7 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -454,6 +454,7 @@ class BIM_PT_animation_tools(Panel): row = self.layout.row() row.alignment = "RIGHT" row.operator("bim.clear_previous_animation", text="Clear Previous Animation", icon="TRACKING_CLEAR_FORWARDS") + row.operator("bim.add_animation_camera", text="Add Camera", icon="CAMERA_DATA") def draw_visualisation_ui(self): row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index acab879966..7af332cbf4 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -584,3 +584,6 @@ def create_baseline(ifc, sequence, work_schedule, name): def clear_previous_animation(sequence): sequence.clear_objects_animation(include_blender_objects=False) + +def add_animation_camera(sequence): + sequence.add_animation_camera() \ No newline at end of file diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 9d644f1a81..44a107f0f0 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -648,6 +648,7 @@ class Search: @interface class Sequence: + def add_animation_camera(cls): pass def add_task_column(cls, column_type, name, data_type): pass def add_text_animation_handler(cls, settings): pass def animate_consumption(cls, obj, start_frame, product_frame, color, animation_type): pass @@ -660,6 +661,7 @@ class Sequence: def animate_operation(cls, obj, start_frame, product_frame, color): pass def animate_output(cls, obj, start_frame, product_frame): pass def clear_object_animation(cls, obj): pass + def clear_object_color(cls, obj): pass def clear_objects_animation(cls, include_blender_objects): pass def contract_all_tasks(cls): pass def contract_task(cls, task): pass @@ -676,6 +678,7 @@ class Sequence: def disable_editing_work_time(cls): pass def disable_selecting_deleted_task(cls): pass def disable_work_schedule(cls): pass + def display_object(cls, obj): pass def enable_editing_rel_sequence_attributes(cls, rel_sequence): pass def enable_editing_sequence_lag_time(cls, rel_sequence): pass def enable_editing_task_animation_colors(cls): pass diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 5cb93acdc7..43582e2723 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -1614,3 +1614,18 @@ class Sequence(blenderbim.core.tool.Sequence): object_type = bpy.context.scene.BIMWorkScheduleProperties.object_type return predefined_type, object_type + @classmethod + def add_animation_camera(cls): + bpy.ops.object.camera_add() + camera = bpy.context.active_object + camera.data.lens = 26 + camera.name = "4D Camera" + camera.location = mathutils.Vector((15, 0, 15)) + camera.rotation_euler = mathutils.Euler((1.2, 0, 1.5), "XYZ") + for obj in bpy.context.scene.objects: + obj.select_set(False) + for obj in bpy.context.visible_objects: + if not (obj.hide_get() or obj.hide_render) and obj.type != "LIGHT": + obj.select_set(True) + bpy.context.scene.camera = camera + bpy.ops.view3d.camera_to_view_selected() diff --git a/src/blenderbim/test/bim/feature/sequence.feature b/src/blenderbim/test/bim/feature/sequence.feature index dc5ddd29af..fba026a0e8 100644 --- a/src/blenderbim/test/bim/feature/sequence.feature +++ b/src/blenderbim/test/bim/feature/sequence.feature @@ -843,3 +843,11 @@ Scenario: Duplicate Task and edit sequence Relationship When I press "bim.enable_editing_task_sequence(task={nested_task_one})" Then nothing happens +Scenario: Add Animation Camera + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I press "bim.add_animation_camera" + Then "scene.objects.get('4D Camera').name" is "4D Camera" From 444a728e7a3f9c40651aa05f8eb60181c22880a9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Aug 2023 22:34:49 +1000 Subject: [PATCH 34/74] Improve default search filter to include types and spatial elements. --- src/blenderbim/blenderbim/tool/search.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/tool/search.py b/src/blenderbim/blenderbim/tool/search.py index 5edeb24e55..ea85c88589 100644 --- a/src/blenderbim/blenderbim/tool/search.py +++ b/src/blenderbim/blenderbim/tool/search.py @@ -55,7 +55,8 @@ class Search(blenderbim.core.tool.Search): comparison, value = cls.get_comparison_and_value(ifc_filter) filter_group_query.append(f"location{comparison}{value}") if not has_instance_or_entity_filter: - filter_group_query.insert(0, "IfcElement") + filter_group_query.insert(0, "IfcProduct") + filter_group_query.insert(0, "IfcTypeProduct") query.append(", ".join(filter_group_query)) return " + ".join(query) From 2bb25cb8749fd9285e77da1b4612c7b6e5bf3b45 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Aug 2023 22:36:23 +1000 Subject: [PATCH 35/74] See #3501. Minor matrix math fix that caused annotations to not appear in global coordinates. --- src/blenderbim/blenderbim/bim/module/drawing/annotation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py index c49dbd8c9d..10e1e59e5a 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py @@ -166,6 +166,7 @@ class Annotator: center = camera.matrix_world.inverted() @ bpy.context.scene.cursor.location center.z = 0 + center = camera.matrix_world @ center return ( center + z_offset, From 3345d7e08803e7c1bf677348083820214ff450fd Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Aug 2023 22:40:46 +1000 Subject: [PATCH 36/74] Fix #2894. Implement reflected ceiling plans. Note that this requires new bot build. --- .../blenderbim/bim/module/drawing/decoration.py | 6 +++++- src/blenderbim/blenderbim/bim/module/drawing/operator.py | 6 ++++-- .../blenderbim/bim/module/drawing/svgwriter.py | 9 ++++++++- src/blenderbim/blenderbim/tool/drawing.py | 9 ++++++++- src/blenderbim/blenderbim/tool/geometry.py | 7 ++++++- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index e392084f8d..9daf4ede50 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -520,7 +520,11 @@ class BaseDecorator: return matrix.inverted()[i].to_3d().normalized() text_dir_world_x_axis = get_basis_vector(obj.matrix_world) - text_dir = (camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + camera_matrix = camera.matrix_world.copy() + camera_matrix[0][0] = 1 + camera_matrix[1][1] = 1 + camera_matrix[2][2] = 1 + text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() pos = location_3d_to_region_2d(region, region3d, text_world_position) props = obj.BIMTextProperties diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 1cbbd8d838..3b16062315 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -485,7 +485,7 @@ class CreateDrawing(bpy.types.Operator): drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element) - self.setup_serialiser(ifc) + self.setup_serialiser(ifc, target_view) cache = IfcStore.get_cache() [cache.remove(guid) for guid in invalidated_guids] tree = ifcopenshell.geom.tree() @@ -829,7 +829,7 @@ class CreateDrawing(bpy.types.Operator): return svg_path - def setup_serialiser(self, ifc): + def setup_serialiser(self, ifc, target_view): self.svg_settings = ifcopenshell.geom.settings( DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True ) @@ -853,6 +853,8 @@ class CreateDrawing(bpy.types.Operator): self.serialiser.setScale(self.scale) self.serialiser.setSubtractionSettings(ifcopenshell.ifcopenshell_wrapper.ALWAYS) self.serialiser.setUsePrefiltering(True) # See #3359 + if target_view == "REFLECTED_PLAN_VIEW": + self.serialiser.setMirrorY(True) # tree = ifcopenshell.geom.tree() # This instructs the tree to explode BReps into faces and return # the style of the face when running tree.select_ray() diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index a0ed721fa5..9b3fad9ee8 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -791,7 +791,14 @@ class SvgWriter: return matrix.inverted()[i].to_3d().normalized() text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world) - text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + + # RCP cameras may be scaled, so reset scales. + camera_matrix = self.camera.matrix_world.copy() + camera_matrix[0][0] = 1 + camera_matrix[1][1] = 1 + camera_matrix[2][2] = 1 + + text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() angle = math.degrees(-text_dir.angle_signed(Vector((1, 0)))) classes = self.get_attribute_classes(text_obj) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 6ad4ba21a7..73e5157171 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -450,7 +450,10 @@ class Drawing(blenderbim.core.tool.Drawing): elif target_view == "REFLECTED_PLAN_VIEW": if location_hint: z = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint)).matrix_world.translation.z - return mathutils.Matrix(((-1, 0, 0, x), (0, 1, 0, y), (0, 0, -1, z + 1.6), (0, 0, 0, 1))) + m = mathutils.Matrix() + m[2][2] = -1 + m.translation = (x, y, z + 1.6) + return m return mathutils.Matrix(((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1))) elif target_view == "ELEVATION_VIEW": if location_hint == "NORTH": @@ -677,6 +680,10 @@ class Drawing(blenderbim.core.tool.Drawing): ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) ) obj.matrix_world = mat + + if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW": + obj.matrix_world[1][1] *= -1 + tool.Geometry.record_object_position(obj) tool.Collector.assign(obj) diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 839f67ffb8..10f04a7f08 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -56,8 +56,13 @@ class Geometry(blenderbim.core.tool.Geometry): @classmethod def clear_scale(cls, obj): + # Note that clearing scale has no impact on cameras. if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4: - if obj.data.users == 1: + if not obj.data: + obj.matrix_world[0][0] = 1 + obj.matrix_world[1][1] = 1 + obj.matrix_world[2][2] = 1 + elif obj.data.users == 1: context_override = {} context_override["object"] = context_override["active_object"] = obj context_override["selected_objects"] = context_override["selected_editable_objects"] = [obj] From 3c5ad1328f2ae56541c6f2e9ea3c6165c6e7ee66 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Aug 2023 22:41:24 +1000 Subject: [PATCH 37/74] Auto create relevant context when attempting to add an annotation to make it friendlier for new users. --- src/blenderbim/blenderbim/core/drawing.py | 7 +++---- src/blenderbim/blenderbim/tool/drawing.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index 6cc977c67c..683f7350d0 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -345,11 +345,10 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None): def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None): - context = drawing_tool.get_annotation_context( - target_view := drawing_tool.get_drawing_target_view(drawing), object_type - ) + target_view = drawing_tool.get_drawing_target_view(drawing) + context = drawing_tool.get_annotation_context(target_view, object_type) if not context: - return f"No annotation context Annotation/{target_view} for drawing" + context = drawing_tool.create_annotation_context(target_view, object_type) drawing_tool.show_decorations() obj = drawing_tool.create_annotation_object(drawing, object_type) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 73e5157171..9ae057a57b 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -345,6 +345,27 @@ class Drawing(blenderbim.core.tool.Drawing): literals.append(literal_data) return literals + @classmethod + def create_annotation_context(cls, target_view, object_type=None): + # checking PLAN target view and annotation type that doesn't require 3d + if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in ( + "FALL", + "SECTION_LEVEL", + "PLAN_LEVEL", + ): + parent = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan") + else: + parent = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model") + + return ifcopenshell.api.run( + "context.add_context", + tool.Ifc.get(), + context_type=parent.ContextType, + context_identifier="Annotation", + target_view=target_view, + parent=parent, + ) + @classmethod def get_annotation_context(cls, target_view, object_type=None): # checking PLAN target view and annotation type that doesn't require 3d From 66fbbeff544e77de83bff9feb7895afd824ea5f4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Aug 2023 22:47:42 +1000 Subject: [PATCH 38/74] See #3561. Fix bug where schedules couldn't be removed. --- src/blenderbim/blenderbim/bim/module/drawing/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 3b16062315..c0bf829553 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -1867,7 +1867,7 @@ class RemoveSchedule(bpy.types.Operator, Operator): schedule: bpy.props.IntProperty() def _execute(self, context): - core.remove_document(tool.Ifc, tool.Drawing, "SCHEDULE", schedule=tool.Ifc.get().by_id(self.schedule)) + core.remove_document(tool.Ifc, tool.Drawing, "SCHEDULE", document=tool.Ifc.get().by_id(self.schedule)) class OpenSchedule(bpy.types.Operator, Operator): From 6263766202f0a5a1daeca92fdfec46168f96ad2b Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:57:18 +0100 Subject: [PATCH 39/74] fix Cost schedule of rates UI bug where linked types dont't refresh correctly (and black formatting) --- src/blenderbim/blenderbim/core/tool.py | 1 - src/blenderbim/blenderbim/tool/cost.py | 25 +++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 44a107f0f0..c19b93508e 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -183,7 +183,6 @@ class Cost: def get_cost_value_unit_component(cls): pass def get_direct_cost_item_products(cls): pass def get_highlighted_cost_item(cls): pass - def get_highlighted_cost_item(cls): pass def get_products(cls, related_object_type): pass def get_schedule_cost_items(cls, cost_schedule): pass def get_units(cls): pass diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index aef65f093c..f6c83b051c 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -156,14 +156,18 @@ class Cost(blenderbim.core.tool.Cost): @classmethod def get_highlighted_cost_item(cls): props = bpy.context.scene.BIMCostProperties + if not props.active_cost_schedule_id: + return if props.active_cost_item_index < len(props.cost_items): return tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id) - return None + return @classmethod def load_cost_item_types(cls, cost_item=None): if not cost_item: - return + cost_item = cls.get_highlighted_cost_item() + if not cost_item: + return props = bpy.context.scene.BIMCostProperties props.cost_item_type_products.clear() # TODO implement process and resource types @@ -220,7 +224,7 @@ class Cost(blenderbim.core.tool.Cost): selected_quantitites = [] unit = "" for quantities in ifcopenshell.util.element.get_psets(product, qtos_only=True).values(): - for qto in (tool.Ifc.get().by_id(quantities["id"]).Quantities or []): + for qto in tool.Ifc.get().by_id(quantities["id"]).Quantities or []: for quantity in cost_item.CostQuantities: if quantity == qto: selected_quantitites.append(quantity) @@ -509,8 +513,9 @@ class Cost(blenderbim.core.tool.Cost): import subprocess import os import sys + if filepath: - path=filepath + path = filepath else: path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", "cost_schedules") @@ -518,6 +523,7 @@ class Cost(blenderbim.core.tool.Cost): os.makedirs(path) if format == "CSV": from ifc5d.ifc5Dspreadsheet import Ifc5DCsvWriter + writer = Ifc5DCsvWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule) writer.write() elif format == "ODS": @@ -528,7 +534,7 @@ class Cost(blenderbim.core.tool.Cost): elif format == "XLSX": from ifc5d.ifc5Dspreadsheet import Ifc5DXlsxWriter - writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule ) + writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule) writer.write() try: if path: @@ -539,7 +545,7 @@ class Cost(blenderbim.core.tool.Cost): elif sys.platform == "linux": subprocess.call(["xdg-open", path]) except: - return 'Could not open file location' + return "Could not open file location" @classmethod def get_units(cls): @@ -566,7 +572,6 @@ class Cost(blenderbim.core.tool.Cost): name = f"{unit.Prefix} {name}" return f"{unit.UnitType} / {name}" - @classmethod def get_cost_schedule(cls, cost_item): for rel in cost_item.HasAssignments or []: @@ -649,6 +654,8 @@ class Cost(blenderbim.core.tool.Cost): @classmethod def toggle_cost_item_parent_change(cls, cost_item=None): + if not cost_item: + return props = bpy.context.scene.BIMCostProperties if props.change_cost_item_parent: props.active_cost_item_id = cost_item.id() @@ -681,12 +688,14 @@ class Cost(blenderbim.core.tool.Cost): if product: cost_items = ifcopenshell.util.cost.get_cost_items_for_product(product) if pset: + def get_products_from_pset(pset): products = [] for rel in pset.DefinesOccurrence or []: if rel.is_a("IfcRelDefinesByProperties"): products.extend(rel.RelatedObjects) return products + products = get_products_from_pset(pset) for product in products or []: cost_items.extend(ifcopenshell.util.cost.get_cost_items_for_product(product)) @@ -705,4 +714,4 @@ class Cost(blenderbim.core.tool.Cost): currency = props.custom_currency return { "Currency": currency, - } \ No newline at end of file + } From 7c06c65df307eb161e227a0b9d3066ae659c84dd Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Wed, 9 Aug 2023 22:02:03 -0700 Subject: [PATCH 40/74] Move Brick "set root" button and change roots on update instead of operator --- .../blenderbim/bim/module/brick/__init__.py | 1 - .../blenderbim/bim/module/brick/operator.py | 14 -------------- .../blenderbim/bim/module/brick/prop.py | 15 +++++++++++++-- src/blenderbim/blenderbim/bim/module/brick/ui.py | 6 +----- src/blenderbim/blenderbim/core/brick.py | 4 +--- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index ca3e084128..ac53d3eb41 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -35,7 +35,6 @@ classes = ( operator.ViewBrickItem, operator.SerializeBrick, operator.AddBrickNamespace, - operator.SetBrickListRoot, operator.RemoveBrickRelation, prop.Brick, prop.BIMBrickProperties, diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 5ca949ffc4..7778fd6eef 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -270,20 +270,6 @@ class AddBrickNamespace(bpy.types.Operator, Operator): core.add_namespace(tool.Brick, alias=alias, uri=uri) -class SetBrickListRoot(bpy.types.Operator, Operator): - bl_idname = "bim.set_brick_list_root" - bl_label = "Set Brick View Type" - bl_options = {"REGISTER", "UNDO"} - split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) - - def _execute(self, context): - if self.split_screen: - root = context.scene.BIMBrickProperties.split_screen_brick_list_root - else: - root = context.scene.BIMBrickProperties.brick_list_root - core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=self.split_screen) - - class RemoveBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick_relation" bl_label = "Remove Relation" diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index 2396b65a59..fed515b044 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -30,6 +30,8 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +import blenderbim.core.brick as core +import blenderbim.tool.brick as tool from blenderbim.tool.brick import BrickStore def update_active_brick_index(self, context): @@ -65,6 +67,15 @@ def get_brick_relations(self, context): return BrickStore.relationships +def update_view(self, context): + root = context.scene.BIMBrickProperties.brick_list_root + core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False) + +def split_screen_update_view(self, context): + root = context.scene.BIMBrickProperties.split_screen_brick_list_root + core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True) + + class Brick(PropertyGroup): name: StringProperty(name="Name") label: StringProperty(name="Label") @@ -79,7 +90,7 @@ class BIMBrickProperties(PropertyGroup): active_brick_index: IntProperty(name="Active Brick Index", update=update_active_brick_index) libraries: EnumProperty(name="Libraries", items=get_libraries) set_list_root_toggled: BoolProperty(name="Set List Root Toggled", default=False) - brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots) + brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots, update=update_view) # namespace manager namespace: EnumProperty(name="Namespace", items=get_namespaces) brick_settings_toggled: BoolProperty(name="Brick Settings Toggled", default=False) @@ -102,4 +113,4 @@ class BIMBrickProperties(PropertyGroup): split_screen_active_brick_index: IntProperty(name="Split Screen Active Brick Index", update=update_active_brick_index) split_screen_active_brick_class: StringProperty(name="Split Screen Active Brick Class") split_screen_brick_breadcrumbs: CollectionProperty(name="Split Screen Brick Breadcrumbs", type=StrProperty) - split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots) \ No newline at end of file + split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index b19e7152d8..20a46e8d45 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -91,6 +91,7 @@ class BIM_PT_brickschema(Panel): row = self.layout.row(align=True) col = row.column() col.alignment = "RIGHT" + row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.prop(data=self.props, property="split_screen_toggled", text="", icon="WINDOW") grid = self.layout.grid_flow(even_columns=True) @@ -99,13 +100,10 @@ class BIM_PT_brickschema(Panel): if len(self.props.brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = False - row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.label(text=self.props.active_brick_class) if self.props.set_list_root_toggled: row = grid1.row(align=True) - op = row.operator("bim.set_brick_list_root", text="Set View") - op.split_screen = False row.prop(data=self.props, property="brick_list_root", text="") row = grid1.row() @@ -122,8 +120,6 @@ class BIM_PT_brickschema(Panel): if self.props.set_list_root_toggled: row = grid2.row(align=True) - op = row.operator("bim.set_brick_list_root", text="Set View") - op.split_screen = True row.prop(data=self.props, property="split_screen_brick_list_root", text="") row = grid2.row() diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index dace67569d..5fea5abe1a 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -132,9 +132,7 @@ def add_namespace(brick, alias=None, uri=None): def set_brick_list_root(brick, brick_root=None, split_screen=False): - brick.clear_brick_browser(split_screen=split_screen) - brick.import_brick_classes(brick_root, split_screen=split_screen) - brick.set_active_brick_class(brick_root, split_screen=split_screen) + brick.run_view_brick_class(brick_class=brick_root, split_screen=split_screen) brick.clear_breadcrumbs(split_screen=split_screen) From 1982069b698b2b9c02deda9aa0c5cdc3305b059f Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:29:12 -0700 Subject: [PATCH 41/74] Add description to Brick operators --- .../blenderbim/bim/module/brick/operator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 7778fd6eef..64358b12a6 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -37,6 +37,7 @@ class LoadBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.load_brick_project" bl_label = "Load Brickschema Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Load in a Brick project from a file" filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"}) @@ -56,6 +57,7 @@ class ViewBrickClass(bpy.types.Operator, Operator): bl_idname = "bim.view_brick_class" bl_label = "View Brick Class" bl_options = {"REGISTER", "UNDO"} + bl_description = "Inspect the subclasses of this class" brick_class: bpy.props.StringProperty(name="Brick Class") split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) @@ -67,6 +69,7 @@ class ViewBrickItem(bpy.types.Operator, Operator): bl_idname = "bim.view_brick_item" bl_label = "View Brick Item" bl_options = {"REGISTER", "UNDO"} + bl_description = "Inspect this entity in the viewer" item: bpy.props.StringProperty(name="Brick Item") split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) @@ -78,6 +81,7 @@ class RewindBrickClass(bpy.types.Operator, Operator): bl_idname = "bim.rewind_brick_class" bl_label = "Rewind Brick Class" bl_options = {"REGISTER", "UNDO"} + bl_description = "Go back to the previous list view" split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) def _execute(self, context): @@ -88,6 +92,7 @@ class CloseBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.close_brick_project" bl_label = "Close Brick Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Close the Brick project" def _execute(self, context): core.close_brick_project(tool.Brick) @@ -122,6 +127,7 @@ class AddBrick(bpy.types.Operator, Operator): bl_idname = "bim.add_brick" bl_label = "Add Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create the Brick entity" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -143,6 +149,7 @@ class AddBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.add_brick_relation" bl_label = "Add Brick Relation" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create the Brick relationship" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -178,6 +185,7 @@ class NewBrickFile(bpy.types.Operator): bl_idname = "bim.new_brick_file" bl_label = "New Brick File" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create a Brick project from scratch" def execute(self, context): IfcStore.begin_transaction(self) @@ -210,6 +218,7 @@ class RefreshBrickViewer(bpy.types.Operator, Operator): bl_idname = "bim.refresh_brick_viewer" bl_label = "Refresh Brick Viewer" bl_options = {"REGISTER", "UNDO"} + bl_description = "Refresh the list view" split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) def _execute(self, context): @@ -220,6 +229,7 @@ class RemoveBrick(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick" bl_label = "Remove Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Delete this entity" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -262,6 +272,7 @@ class SerializeBrick(bpy.types.Operator): class AddBrickNamespace(bpy.types.Operator, Operator): bl_idname = "bim.add_brick_namespace" bl_label = "Add Brick Namespace" + bl_description = "Bind a new namespace to the Brick project" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -274,6 +285,7 @@ class RemoveBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick_relation" bl_label = "Remove Relation" bl_options = {"REGISTER", "UNDO"} + bl_description = "Delete this relationship" predicate: bpy.props.StringProperty(name="Relation") object: bpy.props.StringProperty(name="Object") From 08a5d444d4406271025c57a979c701ac4119a318 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:32:32 -0700 Subject: [PATCH 42/74] Remove another needless BrickStore.graph.triples check As before, this returns a generator type which always evaluates true, even if it is empty. --- src/blenderbim/blenderbim/tool/brick.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 8f308d1590..2f4efd84dc 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -340,10 +340,9 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def remove_brick(cls, brick_uri): - if BrickStore.graph.triples((URIRef(brick_uri), None, None)): - with BrickStore.new_changeset() as cs: - for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): - cs.remove(triple) + with BrickStore.new_changeset() as cs: + for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): + cs.remove(triple) @classmethod def run_assign_brick_reference(cls, element=None, library=None, brick_uri=None): From 60a74335735f59455a37bb9947ebc59ae2518462 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:44:25 -0700 Subject: [PATCH 43/74] Clear Brick breadcrumbs on close project --- src/blenderbim/blenderbim/core/brick.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 5fea5abe1a..821348a3b8 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -51,6 +51,8 @@ def close_brick_project(brick): brick.clear_project() brick.clear_brick_browser() brick.clear_brick_browser(split_screen=True) + brick.clear_breadcrumbs() + brick.clear_breadcrumbs(split_screen=True) def convert_brick_project(ifc, brick): From 429da7c6f3143cff2a9f528b463d26b64b5aae9d Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:51:46 -0700 Subject: [PATCH 44/74] Filter more (all) Brick ontology namespaces I'm not sure if this method of filtering out the entire domain will cause problems. This could use investigation. --- src/blenderbim/blenderbim/tool/brick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 2f4efd84dc..c0b8638471 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -426,7 +426,7 @@ class BrickStore: @classmethod def load_namespaces(cls): BrickStore.namespaces = [] - keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org"] + keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org", "usefulinc.com", "xmlns.com", "opengis.net"] for alias, uri in BrickStore.graph.namespaces(): ignore_namespace = False for keyword in keyword_filter: From d58af0bfa576475bed0254c0864b259a8dfb22b5 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 13:00:02 -0700 Subject: [PATCH 45/74] Filter Brick class if deprecated --- src/blenderbim/blenderbim/tool/brick.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index c0b8638471..8758cc49d1 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -443,8 +443,12 @@ class BrickStore: """ PREFIX brick: PREFIX rdfs: + PREFIX owl: SELECT ?class WHERE { ?class rdfs:subClassOf* brick:{root_class} . + FILTER NOT EXISTS { + ?class owl:deprecated true . + } } """.replace( "{root_class}", root_class From 926970b256189589bd5d68afaef86993655e5915 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 13:32:45 -0700 Subject: [PATCH 46/74] Polish Brick UI and add "last saved" label --- .../blenderbim/bim/module/brick/ui.py | 32 +++++++++++-------- src/blenderbim/blenderbim/tool/brick.py | 11 +++++++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 20a46e8d45..a32ec17bf9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -42,9 +42,17 @@ class BIM_PT_brickschema(Panel): row.operator("bim.load_brick_project", text="Load Project") return + row = self.layout.row(align=True) if BrickStore.path: - row = self.layout.row(align=True) row.label(text=BrickStore.path, icon="FILEBROWSER") + else: + row.label(text="No file", icon="FILEBROWSER") + + row = self.layout.row(align=True) + if BrickStore.last_saved: + row.label(text=BrickStore.last_saved, icon="TIME") + else: + row.label(text="Not saved", icon="TIME") row = self.layout.row(align=True) op = row.operator("bim.serialize_brick", icon="EXPORT", text="Save") @@ -55,7 +63,7 @@ class BIM_PT_brickschema(Panel): row = self.layout.row(align=True) row.prop(data=self.props, property="brick_settings_toggled", text="", icon="PREFERENCES") - + if self.props.brick_settings_toggled: box = self.layout.box() row = box.row(align=True) @@ -86,43 +94,43 @@ class BIM_PT_brickschema(Panel): row.prop(data=self.props, property="new_brick_label", text="") prop_with_search(row, self.props, "brick_entity_class", text="") row.operator("bim.add_brick", text="", icon="ADD") - # row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH") row = self.layout.row(align=True) col = row.column() col.alignment = "RIGHT" row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.prop(data=self.props, property="split_screen_toggled", text="", icon="WINDOW") + row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH") grid = self.layout.grid_flow(even_columns=True) - grid1 = grid.column(align=True) - row = grid1.row(align=True) + grid_left = grid.column(align=True) + row = grid_left.row(align=True) if len(self.props.brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = False row.label(text=self.props.active_brick_class) if self.props.set_list_root_toggled: - row = grid1.row(align=True) + row = grid_left.row(align=True) row.prop(data=self.props, property="brick_list_root", text="") - row = grid1.row() + row = grid_left.row() BIM_UL_bricks.split_screen = False row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index") if self.props.split_screen_toggled: - grid2 = grid.column(align=True) - row = grid2.row(align=True) + grid_right = grid.column(align=True) + row = grid_right.row(align=True) if len(self.props.split_screen_brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = True row.label(text=self.props.split_screen_active_brick_class) if self.props.set_list_root_toggled: - row = grid2.row(align=True) + row = grid_right.row(align=True) row.prop(data=self.props, property="split_screen_brick_list_root", text="") - row = grid2.row() + row = grid_right.row() BIM_UL_bricks.split_screen = True row.template_list("BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index") @@ -161,12 +169,10 @@ class BIM_PT_brickschema(Panel): prop_with_search(row, self.props, "new_brick_relation_type", text="") row.prop(data=self.props, property="new_brick_relation_object", text="") row.operator("bim.add_brick_relation", text="", icon="ADD") - if self.props.brick_create_relations_toggled and self.props.add_relation_failed: row = self.layout.row(align=True) row.label(text="Failed to find this entity!", icon="ERROR") - for relation in BrickschemaData.data["active_relations"]: row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 8758cc49d1..0afac1de09 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -23,6 +23,7 @@ import ifcopenshell.util.brick import blenderbim.core.tool import blenderbim.tool as tool from contextlib import contextmanager +import datetime try: import brickschema @@ -308,6 +309,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.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -378,6 +380,7 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def serialize_brick(cls): BrickStore.get_project().serialize(destination=BrickStore.path, format="turtle") + cls.set_last_saved() @classmethod def add_namespace(cls, alias, uri): @@ -391,11 +394,18 @@ class Brick(blenderbim.core.tool.Brick): else: bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear() + @classmethod + def set_last_saved(cls): + save = os.path.getmtime(BrickStore.path) + save = datetime.datetime.fromtimestamp(save) + BrickStore.last_saved = f"{save.year}-{save.month}-{save.day} {save.hour}:{save.minute}" + class BrickStore: schema = None # this is now a os path path = None # file path if the project was loaded in graph = None # this is the VersionedGraphCollection with 2 arbitrarily named graphs: "schema" and "project" # "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities + last_saved = None history = [] future = [] current_changesets = 0 @@ -415,6 +425,7 @@ class BrickStore: BrickStore.schema = None BrickStore.graph = None BrickStore.path = None + BrickStore.last_saved = None BrickStore.namespaces = [] BrickStore.entity_classes = {} BrickStore.relationships = [] From 0b151ea559e0dc0cca44b2d984f2fc1a00f7584a Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 14:00:56 -0700 Subject: [PATCH 47/74] Second level query for Brick equipment+point roots --- src/blenderbim/blenderbim/tool/brick.py | 36 ++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 0afac1de09..f16e959c4a 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -310,6 +310,7 @@ class Brick(blenderbim.core.tool.Brick): cs.load_file(filepath) BrickStore.path = filepath cls.set_last_saved() + BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -323,6 +324,7 @@ class Brick(blenderbim.core.tool.Brick): with BrickStore.graph.new_changeset("SCHEMA") as cs: cs.load_file(BrickStore.schema) BrickStore.graph.bind("digitaltwin", Namespace("https://example.org/digitaltwin#")) + BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -411,12 +413,7 @@ class BrickStore: current_changesets = 0 history_size = 64 namespaces = [] - root_classes = ["Equipment", - "Electrical_Equipment", "Fire_Safety_Equipment", "HVAC_Equipment", "Lighting_Equipment", "Meter", - "Location", - "System", - "Point", - "Alarm", "Command", "Parameter", "Sensor", "Setpoint", "Status"] + root_classes = ["Equipment", "Location", "System", "Point"] entity_classes = {} relationships = [] @@ -427,6 +424,7 @@ class BrickStore: BrickStore.path = None BrickStore.last_saved = None BrickStore.namespaces = [] + BrickStore.root_classes = ["Equipment", "Location", "System", "Point"] BrickStore.entity_classes = {} BrickStore.relationships = [] @@ -434,6 +432,32 @@ class BrickStore: def get_project(cls): return BrickStore.graph.graph_at(graph="PROJECT") + @classmethod + def load_sub_roots(cls): + query = BrickStore.graph.query( + """ + PREFIX brick: + PREFIX rdfs: + SELECT ?subRoot ?subClasses WHERE { + { + SELECT ?subRoot (COUNT(?subClass) as ?subClasses) WHERE { + { + ?subRoot rdfs:subClassOf brick:Equipment . + } UNION { + ?subRoot rdfs:subClassOf brick:Point . + } + ?subClass rdfs:subClassOf* ?subRoot . + } + GROUP BY ?subRoot + } + FILTER(?subClasses > 3) + } + """ + ) + for row in query: + sub_root = row.get("subRoot").toPython().split("#")[-1] + BrickStore.root_classes.append(sub_root) + @classmethod def load_namespaces(cls): BrickStore.namespaces = [] From d575a43aee0c5651b2299814a2c3664b3c18dcf3 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Fri, 11 Aug 2023 13:01:25 -0700 Subject: [PATCH 48/74] Make refresh Brick viewer refresh both screens --- src/blenderbim/blenderbim/bim/module/brick/operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 64358b12a6..01a66418b9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -219,10 +219,10 @@ class RefreshBrickViewer(bpy.types.Operator, Operator): bl_label = "Refresh Brick Viewer" bl_options = {"REGISTER", "UNDO"} bl_description = "Refresh the list view" - split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) def _execute(self, context): - core.refresh_brick_viewer(tool.Brick, split_screen=self.split_screen) + core.refresh_brick_viewer(tool.Brick) + core.refresh_brick_viewer(tool.Brick, split_screen=True) class RemoveBrick(bpy.types.Operator, Operator): From c45b84c850fe9903320058e9b6ab16180720f291 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 16:16:36 +1000 Subject: [PATCH 49/74] Fix #3577. Don't force uppercase in schedules. --- src/blenderbim/blenderbim/bim/module/drawing/scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py index db501355d3..d7338fd382 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py @@ -351,7 +351,7 @@ class Scheduler: wrap_text: if True, text will be wrapped to fit in cell cell_width: width of cell, used for wrapping text """ - text_lines = [str(p).upper() for p in p_tags] + text_lines = [str(p) for p in p_tags] box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment) text_params = { "font-size": font_size, From b0a5a570a9164ad0027f19e756499e70d92ff725 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 16:40:43 +1000 Subject: [PATCH 50/74] IfcCSV now uses the new filter query system. --- .../blenderbim/bim/module/csv/operator.py | 15 ++++++++------- src/ifccsv/ifccsv.py | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index 71dabc5b21..152c671f39 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -129,9 +129,8 @@ class ExportCsvAttributes(bpy.types.Operator): class ExportIfcCsv(bpy.types.Operator): bl_idname = "bim.export_ifccsv" bl_label = "Export IFC" - #filename_ext = ".csv" + filename_ext = ".csv" filepath: bpy.props.StringProperty(subtype="FILE_PATH") - def invoke(self, context, event): props = context.scene.CsvProperties @@ -149,8 +148,7 @@ class ExportIfcCsv(bpy.types.Operator): ifc_file = IfcStore.get_file() else: ifc_file = ifcopenshell.open(props.csv_ifc_file) - selector = ifcopenshell.util.selector.Selector() - results = selector.parse(ifc_file, props.ifc_selector) + results = ifcopenshell.util.selector.filter_elements(ifc_file, props.ifc_selector) ifc_csv = ifccsv.IfcCsv() attributes = [a.name for a in props.csv_attributes] sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter @@ -196,9 +194,12 @@ class EyedropIfcCsv(bpy.types.Operator): global_ids = [] self.file = IfcStore.get_file() for obj in context.selected_objects: - if hasattr(obj, "BIMObjectProperties") and obj.BIMObjectProperties.ifc_definition_id: - global_ids.append("#" + self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId) - context.scene.CsvProperties.ifc_selector = "|".join(global_ids) + element = tool.Ifc.get_entity(obj) + if element: + global_id = getattr(element, "GlobalId", None) + if global_id: + global_ids.append(global_id) + context.scene.CsvProperties.ifc_selector = ",".join(global_ids) return {"FINISHED"} diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 6497fe3671..08a4e3da58 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -302,7 +302,7 @@ if __name__ == "__main__": parser.add_argument("-i", "--ifc", type=str, required=True, help="The IFC file") parser.add_argument("-s", "--spreadsheet", type=str, default="data.csv", help="The spreadsheet file") parser.add_argument("-f", "--format", type=str, default="csv", help="The format, chosen from csv, ods, or xlsx") - parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as ".IfcWall"') + parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as "IfcWall"') parser.add_argument( "-a", "--arguments", @@ -315,7 +315,7 @@ if __name__ == "__main__": if args.export: ifc_file = ifcopenshell.open(args.ifc) - results = ifcopenshell.util.selector.Selector.parse(ifc_file, args.query) + results = ifcopenshell.util.selector.filter_elements(ifc_file, args.query) ifc_csv = IfcCsv() ifc_csv.export(ifc_file, results, args.arguments or [], output=args.spreadsheet, format=args.format) elif getattr(args, "import"): From 7afd358c3cd8eab33da380caa0c0b8c40f3c7fd4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 16:41:57 +1000 Subject: [PATCH 51/74] Fix #3556. Bug where quantities could not be imported using IfcCSV. Also make IfcCSV support any qset name, not just "Qto" prefixed names. --- src/ifccsv/ifccsv.py | 46 ++++++++++---------------------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 08a4e3da58..d69f890df8 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -58,51 +58,25 @@ class IfcAttributeSetter: return element if "." not in key: return element - if key[0:3] == "Qto": - qto_name, prop = key.split(".", 1) - qto = IfcAttributeSetter.get_element_qto(element, qto_name) - if qto: - IfcAttributeSetter.set_qto_property(qto, prop, value) - return element pset_name, prop = key.split(".", 1) - pset = IfcAttributeSetter.get_element_pset(element, pset_name) + pset = ifcopenshell.util.element.get_pset(element, pset_name, should_inherit=True) if pset: - IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value) - return element + pset = ifc_file.by_id(pset["id"]) + if pset.is_a("IfcElementQuantity"): + IfcAttributeSetter.set_qto_property(pset, prop, value) + else: + IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value) return element - @staticmethod - def get_element_qto(element, name): - for relationship in element.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition - @staticmethod def set_qto_property(qto, name, value): for prop in qto.Quantities: if prop.Name != name: continue - setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", value) - - @staticmethod - def get_element_pset(element, name): - if element.is_a("IfcTypeObject"): - if element.HasPropertySets: - for pset in element.HasPropertySets: - if pset.is_a("IfcPropertySet") and pset.Name == name: - return pset - else: - for relationship in element.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcPropertySet") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition + try: + setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", float(value)) + except: + pass @staticmethod def set_pset_property(ifc_file, pset, name, value): From 694dbeb96c9fd7e2e711d02b2112e3b4cce4b10a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 17:06:22 +1000 Subject: [PATCH 52/74] Fix #3576. Explicit drawing annotations are now immune to "include / exclude" filters. --- src/blenderbim/blenderbim/bim/module/drawing/ui.py | 2 +- src/blenderbim/blenderbim/tool/drawing.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 25155f5096..e96eb9defe 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -314,7 +314,7 @@ class BIM_PT_references(Panel): if not self.props.is_editing_references: row = self.layout.row(align=True) - row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="LONGDISPLAY") + row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="OBJECT_HIDDEN") row.operator("bim.load_references", text="", icon="IMPORT") return diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 9ae057a57b..903794d4fa 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1511,8 +1511,8 @@ class Drawing(blenderbim.core.tool.Drawing): else: base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement")) elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"} - annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) - elements.update(annotations) + annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) + elements.update(annotations) exclude = pset.get("Exclude", None) if exclude: From 04fa59784481adba390eddedda82dc3934a71c5f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 17:09:48 +1000 Subject: [PATCH 53/74] Fix #3564. Bug where IfcBuiltSystems didn't show up due to missing icon in IFC4X3. --- src/blenderbim/blenderbim/bim/module/system/ui.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index b0b411e23a..5b60c151e7 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -123,6 +123,7 @@ class BIM_PT_object_systems(Panel): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } for system in ObjectSystemData.data["systems"]: @@ -284,6 +285,7 @@ class BIM_UL_systems(UIList): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } if item: @@ -316,6 +318,7 @@ class BIM_UL_object_systems(UIList): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } if item: From db7be640d4a4ab23a4b8170ca283988322a043ac Mon Sep 17 00:00:00 2001 From: Massimo Fabbro <79401028+maxfb87@users.noreply.github.com> Date: Sat, 12 Aug 2023 11:03:58 +0200 Subject: [PATCH 54/74] Fix #3569 Variable name override caused the bug (#3575) --- src/blenderbim/blenderbim/bim/module/model/space.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index fb52923279..2af8ae020c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -95,19 +95,19 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True) for obj in bpy.context.visible_objects: - element = tool.Ifc.get_entity(obj) + visible_element = tool.Ifc.get_entity(obj) if ( - not element + not visible_element or obj.type != "MESH" - or not self.is_bounding_class(element) + or not self.is_bounding_class(visible_element) or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal) ): continue old_mesh = None - if element.HasOpenings: - new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, element)) + if visible_element.HasOpenings: + new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, visible_element)) old_mesh = obj.data obj.data = new_mesh From b2eddf55d2ff18a122fa301d2bc2df8a819dcea9 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sat, 12 Aug 2023 12:58:16 +0100 Subject: [PATCH 55/74] Fix contracting/expanding cost schedule tree and add tests to avoid proof changes --- .../blenderbim/bim/module/cost/__init__.py | 83 ++++++++++--------- .../blenderbim/bim/module/cost/operator.py | 11 +++ .../blenderbim/bim/module/cost/ui.py | 4 +- src/blenderbim/blenderbim/tool/cost.py | 4 +- src/blenderbim/test/bim/feature/cost.feature | 48 +++++++++++ 5 files changed, 105 insertions(+), 45 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 8a9ef49798..d279ad317c 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -20,64 +20,65 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddCostColumn, + operator.AddCostItem, + operator.AddCostItemQuantity, operator.AddCostSchedule, - operator.RemoveCostSchedule, - operator.EditCostSchedule, + operator.AddCostValue, + operator.AddCurrency, + operator.AddSummaryCostItem, + operator.AssignCostItemQuantity, + operator.AssignCostItemType, + operator.AssignCostValue, + operator.CalculateCostItemResourceValue, + operator.ChangeParentCostItem, + operator.ClearCostItemAssignments, + operator.ContractCostItem, + operator.ContractCostItemRate, + operator.ContractCostItems, + operator.CopyCostItem, + operator.CopyCostItemValues, + operator.DisableEditingCostItem, + operator.DisableEditingCostItemQuantity, + operator.DisableEditingCostItemValue, + operator.DisableEditingCostSchedule, operator.EditCostItem, operator.EditCostItemQuantity, operator.EditCostItemValue, operator.EditCostItemValueFormula, - operator.EnableEditingCostSchedule, - operator.EnableEditingCostItems, + operator.EditCostSchedule, operator.EnableEditingCostItem, - operator.ExportCostSchedules, - operator.ExpandCostItems, operator.EnableEditingCostItemQuantities, operator.EnableEditingCostItemQuantity, - operator.EnableEditingCostItemValues, + operator.EnableEditingCostItems, operator.EnableEditingCostItemValue, operator.EnableEditingCostItemValueFormula, - operator.DisableEditingCostItem, - operator.DisableEditingCostSchedule, - operator.DisableEditingCostItemQuantity, - operator.DisableEditingCostItemValue, - operator.AddCostColumn, - operator.RemoveCostColumn, - operator.AddCostItem, - operator.AddSummaryCostItem, + operator.EnableEditingCostItemValues, + operator.EnableEditingCostSchedule, operator.ExpandCostItem, - operator.ContractCostItem, + operator.ExpandCostItemRate, + operator.ExpandCostItems, + operator.ExportCostSchedules, + operator.HighlightProductCostItem, + operator.ImportCostScheduleCsv, + operator.LoadCostItemElementQuantities, + operator.LoadCostItemQuantities, + operator.LoadCostItemResourceQuantities, + operator.LoadCostItemTaskQuantities, + operator.LoadCostItemTypes, + operator.LoadProductCostItems, + operator.LoadScheduleOfRates, + operator.RemoveCostColumn, operator.RemoveCostItem, - operator.AssignCostItemType, - operator.UnassignCostItemType, - operator.AssignCostItemQuantity, - operator.UnassignCostItemQuantity, - operator.AddCostItemQuantity, operator.RemoveCostItemQuantity, - operator.AddCostValue, operator.RemoveCostItemValue, - operator.CopyCostItemValues, + operator.RemoveCostSchedule, + operator.ReorderCostItem, operator.SelectCostItemProducts, operator.SelectCostScheduleProducts, - operator.ImportCostScheduleCsv, - operator.LoadCostItemQuantities, - operator.LoadCostItemTypes, - operator.AssignCostValue, - operator.LoadScheduleOfRates, - operator.ExpandCostItemRate, - operator.ContractCostItemRate, - operator.CalculateCostItemResourceValue, - operator.ClearCostItemAssignments, - operator.HighlightProductCostItem, - operator.LoadProductCostItems, - operator.ReorderCostItem, operator.SelectUnassignedProducts, - operator.LoadCostItemElementQuantities, - operator.LoadCostItemTaskQuantities, - operator.LoadCostItemResourceQuantities, - operator.ChangeParentCostItem, - operator.CopyCostItem, - operator.AddCurrency, + operator.UnassignCostItemQuantity, + operator.UnassignCostItemType, prop.CostItem, prop.CostItemQuantity, prop.CostItemType, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 02c7b2e9a8..91bf921347 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -156,6 +156,17 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator): core.contract_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item)) +class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.contract_cost_items" + bl_label = "Contract Cost Item" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Collapse cost item tree" + cost_item: bpy.props.IntProperty() + + def _execute(self, context): + core.contract_cost_items(tool.Cost) + + class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_cost_item" bl_label = "Remove Cost Item" diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 832e69c871..3c5b6e49a5 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -139,8 +139,8 @@ class BIM_PT_cost_schedules(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" row.operator("bim.add_summary_cost_item", text="Add Summary Cost", icon="ADD") - row.operator("bim.expand_all_tasks", text="Expand All") - row.operator("bim.contract_all_tasks", text="Contract All") + row.operator("bim.expand_cost_items", text="Expand All") + row.operator("bim.contract_cost_items", text="Contract All") row = self.layout.row(align=True) row.alignment = "RIGHT" if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items): diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index f6c83b051c..698625bc33 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -96,11 +96,11 @@ class Cost(blenderbim.core.tool.Cost): props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod - def contract_cost_item(cls, cost_item_id): + def contract_cost_item(cls, cost_item): props = bpy.context.scene.BIMCostProperties if not hasattr(cls, "contracted_cost_items"): cls.contracted_cost_items = json.loads(props.contracted_cost_items) - cls.contracted_cost_items.append(cost_item_id) + cls.contracted_cost_items.append(cost_item.id()) props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod diff --git a/src/blenderbim/test/bim/feature/cost.feature b/src/blenderbim/test/bim/feature/cost.feature index 1098c96716..aefe3e5ff9 100644 --- a/src/blenderbim/test/bim/feature/cost.feature +++ b/src/blenderbim/test/bim/feature/cost.feature @@ -3,6 +3,7 @@ Feature: Cost Scenario: Add cost schedule Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" When I press "bim.add_cost_schedule" Then nothing happens @@ -111,6 +112,53 @@ Scenario: Add cost item When I press "bim.add_cost_item(cost_item={cost_item})" Then nothing happens +Scenario: Contract Cost Item + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.contract_cost_item(cost_item={cost_item})" + Then nothing happens + +Scenario: Contract All Cost Items + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.contract_cost_items" + Then nothing happens + +Scenario: Expand Cost Item + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + And I press "bim.contract_cost_item(cost_item={cost_item})" + When I press "bim.expand_cost_item(cost_item={cost_item})" + Then nothing happens + + +Scenario: Expand All Cost Items + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.expand_cost_items" + Then nothing happens + + Scenario: Enable editing cost item quantities Given an empty IFC project And I press "bim.add_cost_schedule" From 5e98dee46fd127a068ded605cffbe65424714270 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 22:25:20 +1000 Subject: [PATCH 56/74] Fix #3554. Support editing IfcIndexedPolyCurve profiles where IfcLineIndex has >2 indices. --- src/blenderbim/blenderbim/tool/model.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index a4e9295d72..904f7019ed 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -358,7 +358,7 @@ class Model(blenderbim.core.tool.Model): is_closed = False if curve.Segments: for segment in curve.Segments: - if len(segment[0]) == 3: # IfcArcIndex + if segment.is_a("IfcArcIndex"): is_arc = True local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) global_point = position @ Vector(local_point).to_3d() @@ -368,12 +368,13 @@ class Model(blenderbim.core.tool.Model): cls.vertices.append(global_point) cls.arcs.append([len(cls.vertices) - 2, len(cls.vertices) - 1]) else: - local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) - global_point = position @ Vector(local_point).to_3d() - cls.vertices.append(global_point) - if is_arc: - cls.arcs[-1].append(len(cls.vertices) - 1) - is_arc = False + for segment_index in segment[0][0:-1]: + local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment_index - 1]) + global_point = position @ Vector(local_point).to_3d() + cls.vertices.append(global_point) + if is_arc: + cls.arcs[-1].append(len(cls.vertices) - 1) + is_arc = False if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]: is_closed = True From 703f0a6b70c1f3084a683066a9635cfe44a0660e Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sat, 12 Aug 2023 14:57:01 +0100 Subject: [PATCH 57/74] fix orphaned data when unassigning element quantities from cost item control --- .../ifcopenshell/api/cost/unassign_cost_item_quantity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py index e4b48bafa0..967cec4162 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py @@ -98,4 +98,7 @@ class Usecase: count = 0 for rel in self.settings["cost_item"].Controls: count += len(rel.RelatedObjects) - quantity[3] = count + if count: + quantity[3] = count + else: + self.file.remove(quantity) From 07b5c4918be53354901dc98deb80b43ecb5c79f9 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sat, 12 Aug 2023 15:17:32 +0100 Subject: [PATCH 58/74] fix api logic where only quantities of the same type can be assignmed to a cost control --- .../api/cost/assign_cost_item_quantity.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index 754b8b02ca..eee40ed06e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -77,19 +77,30 @@ class Usecase: cost_item=item, products=[slab], prop_name="NetVolume") """ self.file = file - self.settings = {"cost_item": cost_item, "products": products or [], "prop_name": prop_name} + self.settings = { + "cost_item": cost_item, + "products": products or [], + "prop_name": prop_name, + } def execute(self): if self.settings["prop_name"]: self.quantities = set(self.settings["cost_item"].CostQuantities or []) + print(self.settings["cost_item"].CostQuantities) for product in self.settings["products"]: - ifcopenshell.api.run( - "control.assign_control", - self.file, - related_object=product, - relating_control=self.settings["cost_item"], - ) if self.settings["prop_name"]: + if ( + self.settings["cost_item"].CostQuantities + and self.settings["cost_item"].CostQuantities[0].Name.lower() + != self.settings["prop_name"].lower() + ): + continue + ifcopenshell.api.run( + "control.assign_control", + self.file, + related_object=product, + relating_control=self.settings["cost_item"], + ) self.add_quantity_from_related_object(product) if self.settings["prop_name"]: self.settings["cost_item"].CostQuantities = list(self.quantities) @@ -107,7 +118,10 @@ class Usecase: if not qto.is_a("IfcElementQuantity"): return for prop in qto.Quantities: - if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == self.settings["prop_name"].lower(): + if ( + prop.is_a("IfcPhysicalSimpleQuantity") + and prop.Name.lower() == self.settings["prop_name"].lower() + ): self.quantities.add(prop) def update_cost_item_count(self): From c1283d2ab01685d65541e80e5d12e0bc58a592e3 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sat, 12 Aug 2023 21:36:40 +0100 Subject: [PATCH 59/74] fix SyntaxError: '(' was never closed --- .../api/profile/add_arbitrary_profile_with_voids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py index a6fecfb55c..0e784ad207 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py @@ -69,7 +69,7 @@ class Usecase: outer_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in outer_points]) inner_curves = [] for inner_point in inner_points: - inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point]) + inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point])) else: outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points)) inner_curves = [] From 84a749605e9c7ef5db13b74efd84e40ccb507a69 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 14:48:36 +1000 Subject: [PATCH 60/74] See #3579. Support Google Colab as an installation option. --- .../docs/ifcopenshell-python/installation.rst | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst index 043fa0689c..439d4cec69 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst @@ -1,16 +1,19 @@ Installation ============ -There are different methods of installation, depending on your situation. +There are different methods of installation, depending on your situation. If +you aren't sure which to choose, if you're a programmer, go for the **Pre-built +packages**. If you aren't a programmer, go for the **BlenderBIM Add-on**. 1. **Pre-built packages** is recommended for users wanting to use the latest IfcOpenShell builds. 2. **PyPI** is recommended for developers using Pip. 3. **Conda** is recommended for developers using Anaconda. 4. **Docker** is recommended for developers using Docker. 5. **AWS Lambda** is recommended for developers using AWS Lambda functions. -6. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface. -7. **From source with precompiled binaries** is recommended for developers actively working with the Python code. -8. **Compiling from source** is recommended for developers actively working with the C++ core. +6. **Google Colab** is recommended for developers using Google Colab. +7. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface. +8. **From source with precompiled binaries** is recommended for developers actively working with the Python code. +9. **Compiling from source** is recommended for developers actively working with the C++ core. Pre-built packages ------------------ @@ -182,6 +185,18 @@ Gateways, etc. the AWS documentation. Some tools that could be useful are AWS CloudFormaton, AWS CDK, pulumi or terraform. +Google Colab +------------ + +The Google Colab environment is based on the distribution from PyPI, but lets +you run it in an online notebook without any local setup required. This is +great for educators and those wanting to try it out without control on their +local system. + +`Click here +`__ +to launch a simple notebook. + Using the BlenderBIM Add-on --------------------------- From 840039ef1e9f1c458f1240e3d9b0c823acd4eda9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 15:49:23 +1000 Subject: [PATCH 61/74] Fix #3472. You can now toggle selectability of linked IFCs. --- .../blenderbim/bim/module/project/__init__.py | 1 + .../blenderbim/bim/module/project/operator.py | 21 +++++++++++++++++++ .../blenderbim/bim/module/project/prop.py | 1 + .../blenderbim/bim/module/project/ui.py | 7 +++++++ 4 files changed, 30 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index df38a8c86d..2bd50dd132 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.SaveLibraryFile, operator.SelectLibraryFile, operator.ToggleFilterCategories, + operator.ToggleLinkSelectability, operator.ToggleLinkVisibility, operator.UnassignLibraryDeclaration, operator.UnlinkIfc, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index a929cb638e..b510494fd7 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -880,6 +880,27 @@ class LoadLink(bpy.types.Operator): return {"FINISHED"} +class ToggleLinkSelectability(bpy.types.Operator): + bl_idname = "bim.toggle_link_selectability" + bl_label = "Toggle Link Selectability" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Toggle selectability" + link: bpy.props.StringProperty() + + def execute(self, context): + props = context.scene.BIMProjectProperties + link = props.links.get(self.link) + for collection in self.get_linked_collections(): + collection.hide_select = not collection.hide_select + link.is_selectable = not collection.hide_select + return {"FINISHED"} + + def get_linked_collections(self): + return [ + c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.link + ] + + class ToggleLinkVisibility(bpy.types.Operator): bl_idname = "bim.toggle_link_visibility" bl_label = "Toggle Link Visibility" diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index e65f5c4e48..bbc3c64f00 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -93,6 +93,7 @@ class FilterCategory(PropertyGroup): class Link(PropertyGroup): name: StringProperty(name="Name") is_loaded: BoolProperty(name="Is Loaded", default=False) + is_selectable: BoolProperty(name="Is Selectable", default=True) is_wireframe: BoolProperty(name="Is Wireframe", default=False) is_hidden: BoolProperty(name="Is Hidden", default=False) diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 788769b159..7558fc3762 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -350,6 +350,13 @@ class BIM_UL_links(UIList): row = layout.row(align=True) if item.is_loaded: row.label(text=item.name) + op = row.operator( + "bim.toggle_link_selectability", + text="", + icon="RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON", + emboss=False, + ) + op.link = item.name op = row.operator( "bim.toggle_link_visibility", text="", From e8437a9c541cfd3c18f0dfcc5cbab1d091ba71ad Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 20:07:33 +1000 Subject: [PATCH 62/74] Fix #3253. Bug where duplicating elements with fillings incorrectly recreated fillings at the cursor (i.e. the cursor was used as a target destination). Now, instead of trying to be clever and regenerating filling locations and opening types, we simply copy the existing scenario. This also means it'll be more stable for weird fillings coming from proprietary apps. --- .../bim/module/geometry/operator.py | 9 +++ src/blenderbim/blenderbim/tool/root.py | 61 +++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 59e47c7c02..8414c6d018 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -637,6 +637,11 @@ class OverrideDuplicateMove(bpy.types.Operator): element = tool.Ifc.get_entity(obj) if element and element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": continue # For now, don't copy drawings until we stabilise a bit more. It's tricky. + + # Prior to duplicating, sync the object placement to make decomposition recreation more stable. + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + new_obj = obj.copy() if obj.data: new_obj.data = obj.data.copy() @@ -705,6 +710,10 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator): relationships = tool.Root.get_decomposition_relationships(context.selected_objects) old_to_new = {} for obj in context.selected_objects: + # Prior to duplicating, sync the object placement to make decomposition recreation more stable. + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + new_obj = obj.copy() if obj.data: new_obj.data = obj.data.copy() diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index b7424ff3bc..ce198c9f00 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -23,6 +23,7 @@ import blenderbim.core.tool import blenderbim.core.geometry import blenderbim.tool as tool from mathutils import Vector +from blenderbim.bim.module.model.opening import FilledOpeningGenerator class Root(blenderbim.core.tool.Root): @@ -57,7 +58,10 @@ class Root(blenderbim.core.tool.Root): if not source.Representation: return dest.Representation = ifcopenshell.util.element.copy_deep( - tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback + tool.Ifc.get(), + source.Representation, + exclude=["IfcGeometricRepresentationContext"], + exclude_callback=exclude_callback, ) elif dest.is_a("IfcTypeProduct"): if not source.RepresentationMaps: @@ -140,9 +144,58 @@ class Root(blenderbim.core.tool.Root): for i, new_subelement in enumerate(new_subelements): new_element = new_elements[i] if data["type"] == "fill": - obj1 = tool.Ifc.get_object(new_element) - obj2 = tool.Ifc.get_object(new_subelement) - bpy.ops.bim.add_filled_opening(voided_obj=obj1.name, filling_obj=obj2.name) + element = new_element + filling = new_subelement + voided_obj = tool.Ifc.get_object(new_element) + filling_obj = tool.Ifc.get_object(new_subelement) + + existing_opening_occurrence = subelement.FillsVoids[0].RelatingOpeningElement + opening = ifcopenshell.api.run( + "root.copy_class", tool.Ifc.get(), product=existing_opening_occurrence + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", + tool.Ifc.get(), + product=opening, + matrix=ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement), + is_si=False, + ) + + representation = ifcopenshell.util.representation.get_representation( + existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" + ) + representation = ifcopenshell.util.representation.resolve_representation(representation) + mapped_representation = ifcopenshell.api.run( + "geometry.map_representation", tool.Ifc.get(), representation=representation + ) + ifcopenshell.api.run( + "geometry.assign_representation", + tool.Ifc.get(), + product=opening, + representation=mapped_representation, + ) + ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element) + ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling) + + voided_objs = [voided_obj] + # Openings affect all subelements of an aggregate + for subelement in ifcopenshell.util.element.get_decomposition(element): + subobj = tool.Ifc.get_object(subelement) + if subobj: + voided_objs.append(subobj) + + for voided_obj in voided_objs: + if voided_obj.data: + representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=voided_obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) @classmethod def run_geometry_add_representation( From eb94c164653e2f5c5a7aac6d499ef64f169e4262 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sun, 13 Aug 2023 12:16:29 +0100 Subject: [PATCH 63/74] fix the bug fix #07b5c49 - where counting objects controlled by a cost item stopped working --- .../api/cost/assign_cost_item_quantity.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index eee40ed06e..71b4cf61f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -88,28 +88,31 @@ class Usecase: self.quantities = set(self.settings["cost_item"].CostQuantities or []) print(self.settings["cost_item"].CostQuantities) for product in self.settings["products"]: + self.assign_cost_control( + related_object=product, cost_item=self.settings["cost_item"] + ) if self.settings["prop_name"]: if ( self.settings["cost_item"].CostQuantities and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower() - ): + ) or not product.is_a("IfcObject"): continue - ifcopenshell.api.run( - "control.assign_control", - self.file, - related_object=product, - relating_control=self.settings["cost_item"], - ) self.add_quantity_from_related_object(product) if self.settings["prop_name"]: self.settings["cost_item"].CostQuantities = list(self.quantities) else: self.update_cost_item_count() + def assign_cost_control(self, related_object, cost_item): + return ifcopenshell.api.run( + "control.assign_control", + self.file, + related_object=related_object, + relating_control=cost_item, + ) + def add_quantity_from_related_object(self, element): - if not element.is_a("IfcObject"): - return for relationship in element.IsDefinedBy: if relationship.is_a("IfcRelDefinesByProperties"): self.add_quantity_from_qto(relationship.RelatingPropertyDefinition) @@ -128,7 +131,7 @@ class Usecase: # This is a bold assumption # https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564 if not self.settings["cost_item"].CostQuantities: - return ifcopenshell.api.run( + ifcopenshell.api.run( "cost.add_cost_item_quantity", self.file, cost_item=self.settings["cost_item"], From 31a12faef1e822ebc01f05b4e895022674841ca2 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sun, 13 Aug 2023 12:18:48 +0100 Subject: [PATCH 64/74] woopsies --- .../ifcopenshell/api/cost/assign_cost_item_quantity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index 71b4cf61f2..b09e7f07fa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -86,7 +86,6 @@ class Usecase: def execute(self): if self.settings["prop_name"]: self.quantities = set(self.settings["cost_item"].CostQuantities or []) - print(self.settings["cost_item"].CostQuantities) for product in self.settings["products"]: self.assign_cost_control( related_object=product, cost_item=self.settings["cost_item"] From 56187e4da3027f758bd00f1e3a3a3eb421e6d9fc Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sun, 13 Aug 2023 13:23:31 +0100 Subject: [PATCH 65/74] Improve Cost Schedule Layout --- .../blenderbim/bim/module/cost/data.py | 11 +++++- .../blenderbim/bim/module/cost/ui.py | 37 ++++++++----------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/data.py b/src/blenderbim/blenderbim/bim/module/cost/data.py index c713b9eb39..9786151c6b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/data.py +++ b/src/blenderbim/blenderbim/bim/module/cost/data.py @@ -117,6 +117,7 @@ class CostSchedulesData: data["TotalAppliedValue"] = 0.0 data["TotalCost"] = 0.0 has_unit_basis = False + is_sum = False if root_element.is_a("IfcCostItem"): values = root_element.CostValues elif root_element.is_a("IfcConstructionResource"): @@ -130,6 +131,11 @@ class CostSchedulesData: data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"] data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"] has_unit_basis = True + else: + data["UnitBasisValueComponent"] = 1 + data["UnitBasisUnitSymbol"] = "U" + if cost_value.Category == "*": + is_sum = True if has_unit_basis: data["TotalCost"] = data["TotalAppliedValue"] / data["UnitBasisValueComponent"] else: @@ -137,7 +143,8 @@ class CostSchedulesData: data["TotalCost"] = data["TotalAppliedValue"] * data["TotalCostQuantity"] else: data["TotalCost"] = data["TotalAppliedValue"] - data["TotalAppliedValue"] = None + if is_sum: + data["TotalAppliedValue"] = None @classmethod def _load_cost_item_quantities(cls, cost_item, data): @@ -154,7 +161,7 @@ class CostSchedulesData: if unit: data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) else: - data["UnitSymbol"] = None + data["UnitSymbol"] = "U" # same_unit_nested_cost_item = set() # data["DerivedTotalCostQuantity"] = None diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 3c5b6e49a5..bdf2e7ef8b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -620,18 +620,22 @@ class BIM_UL_cost_items_trait: else: row.label(text="", icon="DOT") - def draw_total_cost_column(self, layout, cost_item): - format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ") - currency = CostSchedulesData.data["currency"] - text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers - layout.label(text=text) - def draw_quantity_column(self, layout, cost_item): if CostSchedulesData.data["is_editing_rates"]: self.draw_uom_column(layout, cost_item) else: self.draw_total_quantity_column(layout, cost_item) + def draw_uom_column(self, layout, cost_item): + layout.label(text=cost_item["UnitBasisUnitSymbol"]) + + def draw_total_quantity_column(self, layout, cost_item): + if cost_item["TotalCostQuantity"]: + label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}" + layout.label(text=label) + else: + layout.label(text="-") + def draw_value_column(self, layout, cost_item): if cost_item["TotalAppliedValue"]: text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ") @@ -643,8 +647,11 @@ class BIM_UL_cost_items_trait: else: layout.label(text="-") - def draw_uom_column(self, layout, cost_item): - layout.label(text=cost_item["UnitBasisUnitSymbol"] or "-" if cost_item["UnitBasisValueComponent"] else "-") + def draw_total_cost_column(self, layout, cost_item): + format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ") + currency = CostSchedulesData.data["currency"] + text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers + layout.label(text=text) def draw_order_operator(self, row, ifc_definition_id, cost_item): if cost_item["NestingIndex"] is not None: @@ -657,19 +664,7 @@ class BIM_UL_cost_items_trait: op.cost_item = ifc_definition_id op.new_index = cost_item["NestingIndex"] - 1 - def draw_total_quantity_column(self, layout, cost_item): - if cost_item["TotalCostQuantity"]: - label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}" - layout.label(text=label) - else: - layout.label(text="-") - # if cost_item["DerivedTotalCostQuantity"] not in [None, 0]: - # layout.label(text="{0:.2f}".format(cost_item["DerivedTotalCostQuantity"]) + f" {cost_item['DerivedUnitSymbol'] or '-'}") - # else: - # if cost_item["TotalCostQuantity"] == 0: - # layout.label(text="-") - # else: - # layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}") + class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList): From 7edabce65726a109561ff43ef9adc63dd3bfebf4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 23:02:49 +1000 Subject: [PATCH 66/74] Fix #3562. Non-manifold geometry is no longer considered in join criteria as there is no such thing as a closed polygon. --- .../blenderbim/bim/module/drawing/operator.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index c0bf829553..d85cd2dbe9 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -889,6 +889,21 @@ class CreateDrawing(bpy.types.Operator): ) return classes + def is_manifold(self, obj): + result = self.is_manifold_cache.get(obj.data.name, None) + if result is not None: + return result + + bm = bmesh.new() + bm.from_mesh(obj.data) + for edge in bm.edges: + if not edge.is_manifold: + bm.free() + self.is_manifold_cache[obj.data.name] = False + return False + self.is_manifold_cache[obj.data.name] = True + return True + def merge_linework_and_add_metadata(self, root): join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria") if join_criteria: @@ -899,6 +914,7 @@ class CreateDrawing(bpy.types.Operator): group = root.findall(".//{http://www.w3.org/2000/svg}g")[0] joined_paths = {} + self.is_manifold_cache = {} ifc = tool.Ifc.get() for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): @@ -908,6 +924,10 @@ class CreateDrawing(bpy.types.Operator): classes.append("cut") el.set("class", " ".join(classes)) + obj = tool.Ifc.get_object(element) + if not self.is_manifold(obj): + continue + # An element group will contain a bunch of paths representing the # cut of that element. However IfcOpenShell may not correctly # create closed paths. We post-process all paths with shapely to From 8bf55ab46cb1407c5c7a14d5a8067ab4c03a4629 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 23:34:06 +1000 Subject: [PATCH 67/74] See #3561. Misc annotation decorator now only highlights non-internal edges. --- .../blenderbim/bim/module/drawing/decoration.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index 9daf4ede50..07ce208e95 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -248,8 +248,13 @@ class BaseDecorator: if check_mode and obj.data.is_editmode: return self.get_editmesh_geom(obj) - vertices = [obj.matrix_world @ v.co for v in obj.data.vertices] - indices = [e.vertices for e in obj.data.edges] + bm = bmesh.new() + bm.from_mesh(obj.data) + vertices = [obj.matrix_world @ v.co for v in bm.verts] + # In object mode, it's nicer to not show "internal edges". Most will be dissolved anyway. + indices = [[v.index for v in e.verts] for e in bm.edges if len(e.link_faces) != 2] + bm.free() + return vertices, indices def get_editmesh_geom(self, obj): From ed4abacd1e4732ca27dbe063432d4718d4dd036f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 14 Aug 2023 11:55:15 +1000 Subject: [PATCH 68/74] Fix #3578. Support querying surface styles in tools like IfcCSV. --- .../ifcopenshell/util/element.py | 49 +++++++++++++++++++ .../ifcopenshell/util/selector.py | 4 ++ .../test/util/test_element.py | 47 ++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 2e8669b75d..bf0ddf616b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -339,6 +339,8 @@ def get_material(element, should_skip_usage=False, should_inherit=True): The material may be a single material, material set (layered, profiled, or constituent), or a material set usage. + :param element: The element to get the material of. + :type element: ifcopenshell.entity_instance.entity_instance :param should_skip_usage: If set to True, if the material is a material set usage, the material set itself will be returned. Useful if you don't care about occurrence usage parameters. If False, the usage will be @@ -378,6 +380,8 @@ def get_materials(element, should_inherit=True): If the element has a material set, the individual materials of that set are returned as a list. + :param element: The element to get the materials of. + :type element: ifcopenshell.entity_instance.entity_instance :param should_inherit: If True, any inherited materials from associated types will be considered. :return: The associated materials of the element. @@ -403,6 +407,51 @@ def get_materials(element, should_inherit=True): return [c.Material for c in material.MaterialConstituents] +def get_styles(element): + """Retrieves the styles used in an element's representation. + + Styles may be retreived from the material or the body representation. + + :param element: The element to get the styles of. + :type element: ifcopenshell.entity_instance.entity_instance + :return: A list of surface styles + :rtype: list[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + wall = file.by_type("IfcWall")[0] + styles = ifcopenshell.util.element.get_styles(wall) + """ + styles = [] + + materials = ifcopenshell.util.element.get_materials(element) + for material in materials: + for material_definition_representation in material.HasRepresentation or []: + for representation in material_definition_representation.Representations: + for item in representation.Items: + styles.extend([s for s in item.Styles if s.is_a("IfcSurfaceStyle")]) + + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if not body: + return styles + + for representation in [body]: + queue = list(representation.Items) + while queue: + item = queue.pop() + if item.is_a("IfcMappedItem"): + queue.extend(item.MappingSource.MappedRepresentation.Items) + if item.is_a("IfcBooleanResult"): + queue.append(item.FirstOperand) + queue.append(item.SecondOperand) + if item.StyledByItem: + styles.extend([s for s in item.StyledByItem[0].Styles if s.is_a("IfcSurfaceStyle")]) + return styles + + + def get_elements_by_material(ifc_file, material): """Retrieves the elements related to a material. diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 6f940bdf8b..f44160fbf6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -520,6 +520,10 @@ class Selector: value = ifcopenshell.util.element.get_type(value) elif key in ("material", "mat"): value = ifcopenshell.util.element.get_material(value, should_skip_usage=True) + elif key in ("materials", "mats"): + value = ifcopenshell.util.element.get_materials(value) + elif key == "styles": + value = ifcopenshell.util.element.get_styles(value) elif key in ("item", "i"): if value.is_a("IfcMaterialLayerSet"): value = value.MaterialLayers diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index c4e8652472..499f448567 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -386,6 +386,53 @@ class TestGetMaterial(test.bootstrap.IFC4): assert subject.get_material(element, should_inherit=False) is None +class TestGetMaterials(test.bootstrap.IFC4): + def test_getting_the_materials_of_a_product(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + assert subject.get_materials(element) == [material] + + +class TestGetStyles(test.bootstrap.IFC4): + def test_getting_the_styles_of_a_product(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + assert subject.get_styles(element) == [] + + model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") + body = ifcopenshell.api.run("context.add_context", self.file, + context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model) + + material = ifcopenshell.api.run("material.add_material", self.file) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + + style = ifcopenshell.api.run("style.add_style", self.file) + ifcopenshell.api.run("style.add_surface_style", self.file, + style=style, ifc_class="IfcSurfaceStyleShading", attributes={ + "SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 }, + "Transparency": 0., # 0 is opaque, 1 is transparent + }) + ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=body) + + assert subject.get_styles(element) == [style] + + style2 = ifcopenshell.api.run("style.add_style", self.file) + ifcopenshell.api.run("style.add_surface_style", self.file, + style=style2, ifc_class="IfcSurfaceStyleShading", attributes={ + "SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 }, + "Transparency": 0., # 0 is opaque, 1 is transparent + }) + + representation = ifcopenshell.api.run("geometry.add_wall_representation", self.file, + context=body, length=5, height=3, thickness=0.118) + + ifcopenshell.api.run("geometry.assign_representation", self.file, product=element, representation=representation) + ifcopenshell.api.run("style.assign_representation_styles", self.file, shape_representation=representation, styles=[style2]) + + assert subject.get_styles(element) == [style, style2] + + class TestGetElementsByMaterial(test.bootstrap.IFC4): def test_getting_elements_of_a_material(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") From 793b28e43b773e07a92a8fb0fd7c810371d0d97a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 14 Aug 2023 11:04:55 +0500 Subject: [PATCH 69/74] Remove empty IfcDocumentInformationRelationship #3581 --- .../ifcopenshell/api/document/remove_information.py | 7 +++++++ .../test/api/document/test_remove_information.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py index 9506c51478..64fba5975c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py @@ -46,10 +46,17 @@ class Usecase: def execute(self): for reference in self.settings["information"].HasDocumentReferences or []: ifcopenshell.api.run("document.remove_reference", self.file, reference=reference) + for rel in self.settings["information"].IsPointer or []: for information in rel.RelatedDocuments: ifcopenshell.api.run("document.remove_information", self.file, information=information) self.file.remove(rel) + + # remove IfcDocumentInformationRelationship so it won't become invalid + for rel in self.settings["information"].IsPointedTo or []: + if rel.RelatedDocuments == (self.settings["information"],): + self.file.remove(rel) + for rel in self.settings["information"].DocumentInfoForObjects or []: self.file.remove(rel) self.file.remove(self.settings["information"]) 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 4df4857052..d03a7341ad 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_information.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_information.py @@ -37,6 +37,19 @@ class TestRemoveInformation(test.bootstrap.IFC4): assert len(self.file.by_type("IfcDocumentReference")) == 0 assert len(self.file.by_type("IfcRelAssociatesDocument")) == 0 + # test removing relationship to another information if it was the only relating element + information = ifcopenshell.api.run("document.add_information", self.file, parent=None) + information1 = ifcopenshell.api.run("document.add_information", self.file, parent=information) + information2 = ifcopenshell.api.run("document.add_information", self.file, parent=information) + + ifcopenshell.api.run("document.remove_information", self.file, information=information1) + assert len(self.file.by_type("IfcDocumentInformation")) == 2 + assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 1 + + ifcopenshell.api.run("document.remove_information", self.file, information=information2) + assert len(self.file.by_type("IfcDocumentInformation")) == 1 + assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 0 + def test_removing_all_subdocuments_and_their_references_too(self): project = self.file.createIfcProject() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) From bc85f0910984e771dfa9ded7819c009c90b377c8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 14 Aug 2023 11:08:50 +0500 Subject: [PATCH 70/74] Small info message on using validate --- src/blenderbim/blenderbim/bim/module/debug/operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index e8cb0efed8..2b67815858 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -154,6 +154,8 @@ class ValidateIfcFile(bpy.types.Operator): logger = logging.getLogger("validate") logger.setLevel(logging.DEBUG) ifcopenshell.validate.validate(IfcStore.get_file(), logger, express_rules=True) + + self.report({"INFO"}, "Check validation results in the system console.") return {"FINISHED"} From 0ed8dc970db7d06d1264b1b2444e8dd3f0acc87b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 14 Aug 2023 16:14:16 +1000 Subject: [PATCH 71/74] New simplified IfcFM parser for basic object data. --- src/ifcfm/ifcfm/parser.py | 269 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/src/ifcfm/ifcfm/parser.py b/src/ifcfm/ifcfm/parser.py index d055063f69..f74ce4ad78 100644 --- a/src/ifcfm/ifcfm/parser.py +++ b/src/ifcfm/ifcfm/parser.py @@ -22,9 +22,278 @@ import ifcopenshell.util.fm import ifcopenshell.util.selector import ifcopenshell.util.date import ifcopenshell.util.schema +import ifcopenshell.util.system +import ifcopenshell.util.placement import ifcopenshell.util.classification +class Parser2: + def __init__(self, preset="BASIC"): + self.file = None + self.categories = {} + self.get_category_elements = {} + self.get_element_data = {} + self.get_custom_element_data = {} + self.duplicate_keys = [] + + if preset == "BASIC": + self.get_category_elements = { + "actors": get_actors, + "facilities": get_facilities, + "storeys": get_storeys, + "spaces": get_spaces, + "zones": get_zones, + "types": get_types, + "elements": get_elements, + "systems": get_systems, + } + self.get_element_data = { + "actors": get_actor_data, + "facilities": get_facility_data, + "storeys": get_storey_data, + "spaces": get_space_data, + "zones": get_zone_data, + "types": get_type_data, + "elements": get_element_data, + "systems": get_system_data, + } + + def parse(self, ifc_file): + for category_name, get_category_elements in self.get_category_elements.items(): + self.categories.setdefault(category_name, {}) + for element in get_category_elements(ifc_file): + data = self.get_element_data[category_name](ifc_file, element) or {} + custom_data = ( + self.get_custom_element_data.get(category_name, lambda x, y: None)(ifc_file, element) or {} + ) + data.update(custom_data) + + if data: + if data["key"] in self.categories[category_name]: + self.duplicate_keys.append((self.categories[category_name][data["key"]], data)) + self.categories[category_name][data["key"]] = data + + +def get_actors(ifc_file): + return ifc_file.by_type("IfcActor") + + +def get_facilities(ifc_file): + return ifc_file.by_type("IfcBuilding") + + +def get_storeys(ifc_file): + return ifc_file.by_type("IfcBuildingStorey") + + +def get_spaces(ifc_file): + return ifc_file.by_type("IfcSpace") + + +def get_zones(ifc_file): + zones = [] + for zone in ifc_file.by_type("IfcZone"): + for rel in zone.IsGroupedBy: + zones.extend([(zone, space) for space in rel.RelatedObjects]) + return zones + + +def get_types(ifc_file): + return ifcopenshell.util.fm.get_fmhem_types(ifc_file) + + +def get_elements(ifc_file): + elements = set() + for element_type in ifcopenshell.util.fm.get_fmhem_types(ifc_file): + elements.update(ifcopenshell.util.element.get_types(element_type)) + return elements + + +def get_systems(ifc_file): + return ifc_file.by_type("IfcSystem") + + +def get_actor_data(ifc_file, element): + return { + "key": element.TheActor.Name, + "Name": element.TheActor.Name, + "Category": get_classification(element), + "Email": get_actor_address(element, "ElectronicMailAddresses"), + "Phone": get_actor_address(element, "TelephoneNumbers"), + "CompanyURL": get_actor_address(element, "WWWHomePageURL"), + "Department": get_actor_address(element, "InternalLocation"), + "Address1": get_actor_address(element, "AddressLines"), + "Address2": get_actor_address(element, "Town"), + "StateRegion": get_actor_address(element, "Region"), + "PostalCode": get_actor_address(element, "PostalCode"), + "Country": get_actor_address(element, "Country"), + } + + +def get_facility_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(ifc_file.by_type("IfcProject")[0]), + "Category": get_classification(element), + "ProjectName": ifc_file.by_type("IfcProject")[0].Name, + "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), + "LinearUnits": "millimeters", + "AreaUnits": "square meters", + "AreaMeasurement": "BIM Software", + "Phase": ifc_file.by_type("IfcProject")[0].Phase, + "ModelSoftware": get_owner_application(element), + "ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId, + "ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None), + "ModelBuildingID": element.GlobalId, + } + + +def get_storey_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": "Level", + "ModelSoftware": get_owner_application(element), + "ModelObject": element.is_a(), + "ModelID": element.GlobalId, + "Elevation": ifcopenshell.util.placement.get_storey_elevation(element), + } + + +def get_space_data(ifc_file, element): + psets = ifcopenshell.util.element.get_psets(element) + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None), + "Description": element.LongName, + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + "AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2), + "AreaNet": get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2), + } + + +def get_zone_data(ifc_file, element): + zone, space = element + return { + "key": (element.Name or "Unnamed") + (space.Name or "Unnamed"), + "Name": zone.Name, + "AuthorOrganizationName": get_owner_name(zone), + "AuthorDate": get_owner_creation_date(zone), + "SpaceName": space.Name, + "ModelSoftware": get_owner_application(zone), + "ModelID": zone.GlobalId, + } + + +def get_type_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "Description": element.Description, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelTag": element.Tag, + "ModelID": element.GlobalId, + } + + +def get_element_data(ifc_file, element): + space = ifcopenshell.util.element.get_container(element) + space_name = space.Name if space.is_a("IfcSpace") else None + systems = ifcopenshell.util.system.get_element_systems(element) + system = systems[0].Name if systems else None + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "TypeName": ifcopenshell.util.element.get_type(element).Name, + "SpaceName": space_name, + "SystemName": system, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelID": element.GlobalId, + } + + +def get_system_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "Description": element.Description, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + } + + +def get_owner_name(element): + if not getattr(element, "OwnerHistory", None): + return + return element.OwnerHistory.OwningUser.TheOrganization.Name + + +def get_owner_creation_date(element): + if not getattr(element, "OwnerHistory", None): + return + return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + + +def get_owner_application(element): + if not getattr(element, "OwnerHistory", None): + return + return element.OwnerHistory.OwningApplication.ApplicationFullName + + +def get_facility_parent(element, ifc_class): + parent = ifcopenshell.util.element.get_aggregate(element) + while parent: + if parent.is_a(ifc_class): + return parent + if parent.is_a("IfcProject"): + return + parent = ifcopenshell.util.element.get_aggregate(parent) + + +def get_classification(element): + references = list(ifcopenshell.util.classification.get_references(element)) + if references: + if hasattr(references[0], "Identification"): + return "{}:{}".format(references[0].Identification, references[0].Name) + return "{}:{}".format(references[0].ItemReference, references[0].Name) + + +def get_actor_address(element, name): + for address in element.TheActor.Addresses or []: + if hasattr(address, name) and getattr(address, name, None): + result = getattr(address, name) + if isinstance(result, tuple): + return result[0] + return result + + +def get_property(psets, pset_name, prop_name, decimals=None): + if pset_name in psets: + result = psets[pset_name].get(prop_name, None) + if decimals is None or result is None: + return result + return round(result, decimals) + + class Parser: def __init__(self, logger): self.logger = logger From 6878b2c0725ebc7109d97afe6e1d62c805185458 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 14 Aug 2023 17:46:10 +0500 Subject: [PATCH 72/74] Handle ods schedules without column styles #3573 Turned out there are ODS files (and turned out we're producing them ourselves from ifccsv) where column styles are not defined and therefore no width/style assigned to each column which was resulting in errors on building schedule. --- .../bim/module/drawing/scheduler.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py index d7338fd382..a703075772 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py @@ -118,6 +118,25 @@ class Scheduler: if cell_style: related_styles.append((style_name, cell_style)) + # sometimes there are no column styles (e.g. in ODS from IfcCSV) + # and we just use some constant number for column widths + if not column_widths: + row_columns = [] + for tr in table.getElementsByType(TableRow): + row_cols = 0 + for td in tr.getElementsByType(TableCell): + column_span = td.getAttribute("numbercolumnsspanned") + column_span = int(column_span) if column_span else 1 + + col_repeat = td.getAttribute("numbercolumnsrepeated") + col_repeat = int(col_repeat) if col_repeat else 1 + row_cols += column_span * col_repeat + row_columns.append(row_cols) + + n_columns = max(row_columns) + column_widths = [25] * n_columns # some constant width value 👀 + column_styles = [None] * n_columns + # collect rows height row_heights = [] # TODO: never used yet because unsure about priority for row styles From 2ed24434c88ee6000c400a6d150a295c932834bd Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:21:44 +0100 Subject: [PATCH 73/74] experimental import plot coordinates --- .../bim/module/georeference/__init__.py | 1 + .../bim/module/georeference/operator.py | 19 ++++++++++- .../blenderbim/bim/module/misc/ui.py | 3 +- .../blenderbim/core/georeference.py | 5 ++- .../blenderbim/tool/georeference.py | 34 +++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py index 146ce8b1c3..6a1c3fb1b6 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py @@ -34,6 +34,7 @@ classes = ( operator.GetCursorLocation, operator.SetCursorLocation, operator.ConvertAngleToCoordinates, + operator.ImportPlot, prop.BIMGeoreferenceProperties, ui.BIM_PT_gis, ui.BIM_PT_gis_utilities, diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index cbcfe930ea..eec2dab9ca 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -110,7 +110,7 @@ class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_cursor_location" bl_label = "Set Cursor Location" bl_options = {"REGISTER", "UNDO"} - bl_description = "Move curson location to the specified coordinates" + bl_description = "Move cursor location to the specified coordinates" @classmethod def poll(cls, context): @@ -187,3 +187,20 @@ class ConvertAngleToCoordinates(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.convert_angle_to_coord(tool.Georeference, type=self.type) + + +class ImportPlot(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.import_plot" + bl_label = "Import Plot" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Import plot" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + + def execute(self, context): + core.import_plot(tool.Georeference, filepath=self.filepath) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} diff --git a/src/blenderbim/blenderbim/bim/module/misc/ui.py b/src/blenderbim/blenderbim/bim/module/misc/ui.py index 9fe4e178cc..a2c9ebb0ec 100644 --- a/src/blenderbim/blenderbim/bim/module/misc/ui.py +++ b/src/blenderbim/blenderbim/bim/module/misc/ui.py @@ -50,8 +50,9 @@ class BIM_PT_misc_utilities(bpy.types.Panel): row.operator("bim.clean_wireframes") row = layout.row() row.operator("bim.patch_non_parametric_mep_segment") - row = layout.row(align=True) row.operator("bim.enable_editing_sketch_extrusion_profile", text="Start Sketching") row.operator("bim.edit_sketch_extrusion_profile", text="", icon="FILE_REFRESH") row.operator("bim.disable_editing_sketch_extrusion_profile", text="", icon="CANCEL") + row = layout.row() + row.operator("bim.import_plot", text="Import Plot Coordinates", icon="FILE_FOLDER") diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index f3faded3b2..dcc9823904 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -83,4 +83,7 @@ def convert_global_to_local(georeference): def convert_angle_to_coord(georeference, type): vector_coordinates = georeference.angle2coords(georeference.get_angle(type), type) - georeference.set_vector_coordinates(vector_coordinates,type) \ No newline at end of file + georeference.set_vector_coordinates(vector_coordinates,type) + +def import_plot(georeference, filepath): + georeference.import_plot(filepath, georeference.get_map_conversion()) diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py index 2dc6001b61..52c31de1a2 100644 --- a/src/blenderbim/blenderbim/tool/georeference.py +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -296,3 +296,37 @@ class Georeference(blenderbim.core.tool.Georeference): elif type == "rel_y": bpy.context.scene.BIMGeoreferenceProperties.y_axis_abscissa_output = str(x) bpy.context.scene.BIMGeoreferenceProperties.y_axis_ordinate_output = str(y) + + @classmethod + def import_plot(cls, filepath, map_conversion): + import bmesh + + def parse_csv(file_path): + import csv + + with open(file_path, "r") as f: + reader = csv.reader(f) # Assuming tab-delimited CSV + rows = [] + for row in reader: + if len(row) == 0: + continue + rows.append(row) + return rows + + rows = parse_csv(filepath) + vertices = [] + for row in rows: + coordinates = cls.enh2xyz([float(row[0]), float(row[1]), float(row[2])], map_conversion) + vertices.append(coordinates) + + mesh = bpy.data.meshes.new("mesh") + obj = bpy.data.objects.new("Plot Line", mesh) + bpy.context.scene.collection.objects.link(obj) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obj.data + bm = bmesh.new() + for vertex in vertices: + bm.verts.new(vertex) + bm.to_mesh(mesh) + bm.free() From f523870b578ee5336e14547bccc7676fcfe1a165 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 15 Aug 2023 09:55:48 +1000 Subject: [PATCH 74/74] Fix #3584. Support matching enumerated properties in new facet selector. --- src/ifcopenshell-python/ifcopenshell/util/selector.py | 4 +++- src/ifcopenshell-python/test/util/test_selector.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index f44160fbf6..2f08dc4049 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -294,7 +294,9 @@ class FacetTransformer(lark.Transformer): return False def compare(self, element_value, comparison, value): - if isinstance(value, str): + if isinstance(element_value, (list, tuple)): + return any(self.compare(ev, comparison, value) for ev in element_value) + elif isinstance(value, str): if isinstance(element_value, int): value = int(value) elif isinstance(element_value, float): diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 0ff7227b47..67749417bd 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -137,6 +137,9 @@ class TestFilterElements(test.bootstrap.IFC4): ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Baz": 123}) assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz=123") == {element} ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Bay": 123.3}) + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["New"]}) + assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element} def test_selecting_by_classification(self): project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")