Merge branch 'v0.6.0' into GSoC#45-IDS-checking

This commit is contained in:
ArturTomczak
2021-06-21 08:48:46 +02:00
committed by GitHub
244 changed files with 15113 additions and 5939 deletions
@@ -0,0 +1,250 @@
import operator
from dataclasses import dataclass
import numpy
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.express
import ifcopenshell.transition_curve
# geometric primitives
# @notes
# - not sure if the separation of geometric primitives make sense
# does it make handling the variety of distance expressions and
# interpolation harder?
@dataclass
class line:
start_point: numpy.ndarray
direction_vector: numpy.ndarray
def __call__(self, u):
p = numpy.ndarray((3,))
p[0:2] = self.start_point + self.direction_vector * u
p[2] = numpy.nan
return p
@dataclass
class circle:
radius: numpy.ndarray
def __call__(self, u):
return numpy.array(
[self.radius * numpy.cos(u), self.radius * numpy.sin(u), numpy.nan]
)
def place(matrix, func):
"""
Higher order function for application of a 3x3 matrix
to a 2D point. Assumes a functor such as line or circle.
"""
def inner(*args):
v = func(*args)
# homogenize
v = numpy.insert(v[0:2], v[0:2].shape, 1, axis=-1)
p = numpy.ndarray((3,))
p[0:2] = (matrix @ v)[0:2]
p[2] = numpy.nan
return p
return inner
# primitives for manipulating and joining curve functor domains
def reparametrized_curve(fn, a, b):
return lambda u: fn(a * u + b)
def normalized_curve(fn):
return lambda u: fn(u / fn.length)
class trimmed_curve:
def __init__(self, fn, length):
self.fn = fn
self.length = length
def __call__(self, u):
assert u >= 0.0 and u <= self.length
return self.fn(u)
class piecewise:
# takes a set of functors and returns a function f(u) that delegates to the correct segment
def __init__(self, fns):
self.fns = fns
self.length = sum(map(operator.attrgetter("length"), fns))
def __call__(self, u):
# this is silly, assuming `u` is monotonically increases we should not always start
# searching from the first segment or at least binary search into the segment
# lengths
u0 = 0
for fn in self.fns:
u1 = u0 + fn.length
if u >= u0 and u <= u1:
return fn(u - u0)
u0 = u1
# mapping functions from IFC entities
def map_inst(inst):
"""
Looks up one of the implementation functions below in the global namespace
"""
return globals()[f"impl_{inst.is_a()}"](inst)
def impl_IfcLine(inst):
return line(
numpy.array(inst.Pnt.Coordinates),
numpy.array(inst.Dir.Orientation.DirectionRatios) * inst.Dir.Magnitude,
)
def impl_IfcCircle(inst):
return place(map_inst(inst.Position), circle(inst.Radius))
def impl_IfcClothoid(inst):
# @todo
# place = map_inst(inst.Position)
# ifcopenshell.transition_curve.TransitionCurve(
# StartPoint = place.T[2]
# StartDirection = numpy.arctan2(place.T[0][1], place.T[0][0]),
# SegmentLength =
# IsStartRadiusCCW =
# IsEndRadiusCCW =
# TransitionCurveType =
# StartRadius =
# EndRadius =
# )
return lambda *args: numpy.array((0.0, 0.0))
def impl_IfcAxis2Placement2D(inst):
arr = numpy.eye(3)
if inst is None:
return arr
arr.T[2, 0:2] = inst.Location.Coordinates
if inst.RefDirection is None:
return arr
arr.T[0, 0:2] = inst.RefDirection.DirectionRatios
arr.T[0, 0:2] /= numpy.linalg.norm(arr.T[0, 0:2])
arr.T[1, 0:2] = -arr.T[0, 1], arr.T[0, 0]
return arr
# conversion functions for semantic design parameters (not used atm)
def convert(inst):
"""
Looks up one of the conversion functions below in the global namespace
"""
yield from globals()[f"convert_{inst.is_a()}_{inst.PredefinedType}"](inst)
def convert_IfcAlignmentHorizontalSegment_LINE(data):
xy = numpy.array(data.StartPoint.Coordinates)
yield xy
di = numpy.array([numpy.cos(data.StartDirection), numpy.sin(data.StartDirection)])
yield xy + di * data.SegmentLength
# Two approaches, either DesignParameters or Representation
def interpret_linear_element_semantics(settings, crv):
# traverse decomposition
for rel in crv.IsNestedBy:
for obj in rel.RelatedObjects:
yield from interpret_linear_element_semantics(settings, obj)
# lookup design parameters and dispatch to conversion function
if crv.is_a("IfcAlignmentSegment"):
dp = crv.DesignParameters
yield from convert(dp)
def evaluate_segment(segment):
# print(segment)
# print(segment.ParentCurve)
# print()
func = place(map_inst(segment.Placement), map_inst(segment.ParentCurve))
# reparam so domain starts at zero
reparam = reparametrized_curve(func, 1.0, -segment.SegmentStart[0])
# embed curve length (doesn't do much, just make length recoverable)
trimmed = trimmed_curve(reparam, segment.SegmentLength[0])
return trimmed
def interpret_linear_element_geometry(settings, crv):
func = piecewise(
list(
map(
evaluate_segment,
crv.Representation.Representations[0].Items[0].Segments,
)
)
)
for u in numpy.linspace(0, func.length, num=int(numpy.ceil(func.length / 0.05))):
yield func(u)
interpret_linear_element = interpret_linear_element_geometry
def create_shape(settings, elem):
if elem.is_a("IfcLinearPositioningElement") or elem.is_a("IfcLinearElement"):
return numpy.row_stack(list(interpret_linear_element(settings, elem)))
else:
return ifcopenshell.geom.create_shape(settings, elem)
def print_structure(alignment, indent=0):
"""
Debugging function to print alignment decomposition
"""
print(" " * indent, str(alignment)[0:100])
for rel in alignment.IsNestedBy:
for child in rel.RelatedObjects:
print_structure(child, indent + 2)
if __name__ == "__main__":
import sys
from matplotlib import pyplot as plt
s = ifcopenshell.express.parse("IFC4x3_RC3.exp")
ifcopenshell.register_schema(s)
f = ifcopenshell.open(sys.argv[1])
print_structure(f.by_type("IfcAlignment")[0])
al_hor = f.by_type("IfcAlignmentHorizontal")[0]
xy = create_shape({}, al_hor)
plt.plot(xy.T[0], xy.T[1])
plt.savefig("horizontal_alignment.png")
@@ -1,11 +1,99 @@
import json
import numpy
import importlib
import ifcopenshell
import ifcopenshell.api
def run(usecase_path, ifc_file=None, **settings):
pre_listeners = {}
post_listeners = {}
def run(usecase_path, ifc_file=None, should_run_listeners=True, **settings):
if should_run_listeners:
for listener in pre_listeners.get(usecase_path, {}).values():
listener(usecase_path, ifc_file, settings)
def serialise_entity_instance(entity):
return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)}
vcs_settings = settings.copy()
for key, value in settings.items():
if isinstance(value, ifcopenshell.entity_instance):
vcs_settings[key] = serialise_entity_instance(value)
elif isinstance(value, numpy.ndarray):
vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
vcs_settings[key] = [serialise_entity_instance(i) for i in value]
if "add_representation" in usecase_path:
pass
# print(usecase_path, "{ ... settings too complex right now ... }")
elif "owner." in usecase_path:
pass
else:
pass
# print(vcs_settings)
# try:
# print(usecase_path, json.dumps(vcs_settings))
# except:
# print(usecase_path, vcs_settings)
importlib.import_module(f"ifcopenshell.api.{usecase_path}")
module, usecase = usecase_path.split(".")
usecase_class = getattr(getattr(getattr(ifcopenshell.api, module), usecase), "Usecase")
if ifc_file:
return usecase_class(ifc_file, **settings).execute()
return usecase_class(**settings).execute()
result = usecase_class(ifc_file, **settings).execute()
else:
result = usecase_class(**settings).execute()
if should_run_listeners:
for listener in post_listeners.get(usecase_path, {}).values():
listener(usecase_path, ifc_file, settings)
return result
def add_pre_listener(usecase_path, name, callback):
"""Add a pre listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function
"""
pre_listeners.setdefault(usecase_path, {})[name] = callback
def add_post_listener(usecase_path, name, callback):
"""Add a post listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function
"""
post_listeners.setdefault(usecase_path, {})[name] = callback
def remove_pre_listener(usecase_path, name, callback):
"""Remove a pre listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function
"""
pre_listeners.get(usecase_path, {}).pop(name, None)
def remove_post_listener(usecase_path, name, callback):
"""Remove a post listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function
"""
post_listeners.get(usecase_path, {}).pop(name, None)
def remove_all_listeners():
pre_listeners.clear()
post_listeners.clear()
@@ -0,0 +1,33 @@
class Data:
is_loaded = False
boundaries = {}
spaces = {}
@classmethod
def purge(cls):
cls.is_loaded = False
cls.boundaries = {}
cls.spaces = {}
@classmethod
def load(cls, file):
cls._file = file
for boundary in cls._file.by_type("IfcRelSpaceBoundary"):
data = boundary.get_info()
data["RelatingSpace"] = data["RelatingSpace"].id() if data["RelatingSpace"] else None
data["RelatedBuildingElement"] = (
data["RelatedBuildingElement"].id() if data["RelatedBuildingElement"] else None
)
del data["ConnectionGeometry"]
if cls._file.schema == "IFC2X3":
pass
else:
if boundary.is_a("IfcRelSpaceBoundary1stLevel"):
data["ParentBoundary"] = data["ParentBoundary"].id() if data["ParentBoundary"] else None
if boundary.is_a("IfcRelSpaceBoundary2ndLevel"):
data["CorrespondingBoundary"] = (
data["CorrespondingBoundary"].id() if data["CorrespondingBoundary"] else None
)
cls.boundaries[boundary.id()] = data
cls.spaces.setdefault(data["RelatingSpace"], []).append(boundary.id())
cls.is_loaded = True
@@ -1,16 +0,0 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"cost_item": None, "ifc_class": "IfcQuantityCount"}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
value = self.file.create_entity("IfcCostValue")
values = list(self.settings["cost_item"].CostValues or [])
values.append(value)
self.settings["cost_item"].CostValues = values
return value
@@ -0,0 +1,18 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"parent": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
value = self.file.create_entity("IfcCostValue")
if self.settings["parent"].is_a("IfcCostItem"):
values = list(self.settings["parent"].CostValues or [])
values.append(value)
self.settings["parent"].CostValues = values
elif self.settings["parent"].is_a("IfcCostValue"):
values = list(self.settings["parent"].Components or [])
values.append(value)
self.settings["parent"].Components = values
return value
@@ -0,0 +1,31 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"cost_item": None, "products": []}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
quantity_names = set()
for quantity in self.settings["cost_item"].CostQuantities or []:
if quantity.Name:
quantity_names.add(quantity.Name)
for product in self.settings["products"]:
ifcopenshell.api.run(
"control.assign_control",
self.file,
related_object=product,
relating_control=self.settings["cost_item"],
)
for name in quantity_names:
ifcopenshell.api.run(
"cost.assign_cost_item_product_quantities",
self.file,
cost_item=self.settings["cost_item"],
prop_name=name
)
@@ -4,7 +4,7 @@ import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"cost_item": None, "qto_name": "", "prop_name": ""}
self.settings = {"cost_item": None, "prop_name": ""}
for key, value in settings.items():
self.settings[key] = value
@@ -25,7 +25,7 @@ class Usecase:
self.add_quantity_from_qto(relationship.RelatingPropertyDefinition)
def add_quantity_from_qto(self, qto):
if not qto.is_a("IfcElementQuantity") or qto.Name.lower() != self.settings["qto_name"].lower():
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():
@@ -0,0 +1,17 @@
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"source": None, "destination": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for cost_value in self.settings["destination"].CostValues or []:
ifcopenshell.api.run("cost.remove_cost_item_value", self.file, cost_value=cost_value)
copied_cost_values = []
for cost_value in self.settings["source"].CostValues or []:
copied_cost_values.append(ifcopenshell.util.element.copy_deep(self.file, cost_value))
self.settings["destination"].CostValues = copied_cost_values
@@ -87,12 +87,31 @@ class Data:
value_data["Components"] = [c.id() for c in value_data["Components"] or []]
value_data["AppliedValue"] = cls.calculate_applied_value(cost_item, cost_value)
cls.cost_values[cost_value.id()] = value_data
for component in cost_value.Components or []:
cls.load_cost_item_value(cost_item, component)
@classmethod
def calculate_applied_value(cls, cost_item, cost_value, category_filter=None):
result = 0
if cost_value.ArithmeticOperator and cost_value.Components:
pass # TODO
component_values = []
for component in cost_value.Components:
component_values.append(cls.calculate_applied_value(cost_item, component, category_filter))
if cost_value.ArithmeticOperator == "ADD":
return sum(component_values)
result = component_values.pop(0)
if cost_value.ArithmeticOperator == "DIVIDE":
for value in component_values:
try:
result /= value
except ZeroDivisionError:
pass
elif cost_value.ArithmeticOperator == "MULTIPLY":
for value in component_values:
result *= value
elif cost_value.ArithmeticOperator == "SUBTRACT":
for value in component_values:
result -= value
return result
if cost_value.Category is None:
return cls.get_primitive_applied_value(cost_value.AppliedValue)
elif cost_value.Category == "*":
@@ -105,7 +124,7 @@ class Data:
return cls.sum_child_cost_items(cost_item, category_filter=cost_value.Category)
else:
return cls.get_primitive_applied_value(cost_value.AppliedValue)
return result
return 0
@classmethod
def sum_child_cost_items(cls, cost_item, category_filter=None):
@@ -1,3 +1,6 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
@@ -7,4 +10,13 @@ class Usecase:
def execute(self):
# TODO: do a deep purge
for inverse in self.file.get_inverse(self.settings["cost_item"]):
if inverse.is_a("IfcRelNests"):
if inverse.RelatingObject == self.settings["cost_item"]:
for related_object in inverse.RelatedObjects:
ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=related_object)
elif inverse.RelatedObjects == tuple(self.settings["cost_item"]):
self.file.remove(inverse)
elif inverse.is_a("IfcRelAssignsToControl"):
self.file.remove(inverse)
self.file.remove(self.settings["cost_item"])
@@ -0,0 +1,29 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"cost_item": None, "products": []}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for quantity in self.settings["cost_item"].CostQuantities or []:
for inverse in self.file.get_inverse(quantity):
if not inverse.is_a("IfcElementQuantity"):
continue
for rel in inverse.DefinesOccurrence or []:
for related_object in rel.RelatedObjects:
if related_object in self.settings["products"]:
self.quantities.remove(quantity)
self.settings["cost_item"].CostQuantities = list(self.quantities)
for product in self.settings["products"]:
ifcopenshell.api.run(
"control.unassign_control",
self.file,
related_object=product,
relating_control=self.settings["cost_item"],
)
@@ -1,9 +1,13 @@
import bpy
import bmesh
import ifcopenshell.util.unit
from mathutils import Vector
from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper
Z_AXIS = Vector((0, 0, 1))
X_AXIS = Vector((1, 0, 0))
EPSILON = 1e-6
class Usecase:
def __init__(self, file, **settings):
@@ -25,15 +29,20 @@ class Usecase:
# IfcExtrudedAreaSolid/IfcRectangleProfileDef
# IfcExtrudedAreaSolid/IfcCircleProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids
# IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage
"ifc_representation_class": None, # Whether to cast a mesh into a particular class
"profile_set_usage": None, # The material profile set if the extrusion requires it
}
self.ifc_vertices = []
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if isinstance(self.settings["geometry"], bpy.types.Mesh):
if (
isinstance(self.settings["geometry"], bpy.types.Mesh)
and self.settings["geometry"] == self.settings["blender_object"].data
):
self.evaluate_geometry()
if self.settings["unit_scale"] is None:
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -43,35 +52,45 @@ class Usecase:
return self.create_plan_representation()
return self.create_variable_representation()
def evaluate_geometry(self):
self.boolean_modifiers = []
for modifier in self.settings["blender_object"].modifiers:
if not modifier.type == "BOOLEAN":
continue
modifier_data = {}
for name in ["operation", "operand_type", "object", "solver", "use_self"]:
modifier_data[name] = getattr(modifier, name)
self.boolean_modifiers.append(modifier_data)
self.settings["blender_object"].modifiers.remove(modifier)
if self.settings["should_force_triangulation"]:
mesh = self.settings["blender_object"].evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh()
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(mesh)
bm.free()
del bm
self.settings["geometry"] = mesh
def should_triangulate_face(self, face, threshold=EPSILON):
vz = face.normal
co = face.verts[0].co
if vz.length < 0.5:
return True
if abs(vz.z) < 0.5:
vx = vz.cross(Z_AXIS)
else:
self.settings["geometry"] = (
self.settings["blender_object"].evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh()
)
vx = vz.cross(X_AXIS)
vy = vx.cross(vz)
tM = Matrix(
[[vx.x, vy.x, vz.x, co.x], [vx.y, vy.y, vz.y, co.y], [vx.z, vy.z, vz.z, co.z], [0, 0, 0, 1]]
).inverted()
for modifier in self.boolean_modifiers:
new = self.settings["blender_object"].modifiers.new("IfcOpeningElement", "BOOLEAN")
for key, value in modifier.items():
setattr(new, key, value)
return any([abs((tM @ v.co).z) > threshold for v in face.verts])
def evaluate_geometry(self):
for modifier in self.settings["blender_object"].modifiers:
if modifier.type == "BOOLEAN":
modifier.show_viewport = False
mesh = self.settings["blender_object"].evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh()
bm = bmesh.new()
bm.from_mesh(mesh)
if self.settings["should_force_triangulation"]:
faces = bm.faces
else:
faces = [f for f in bm.faces if self.should_triangulate_face(f)]
bmesh.ops.triangulate(bm, faces=faces)
bm.to_mesh(mesh)
mesh.update()
bm.free()
del bm
self.settings["geometry"] = mesh
for modifier in self.settings["blender_object"].modifiers:
if modifier.type == "BOOLEAN":
modifier.show_viewport = True
def create_model_representation(self):
if self.settings["context"].is_a() == "IfcGeometricRepresentationContext":
@@ -97,6 +116,8 @@ class Usecase:
return self.create_curve3d_representation()
elif self.settings["context"].ContextIdentifier == "SurveyPoints":
return self.create_geometric_curve_set_representation()
elif self.settings["context"].ContextIdentifier == "Lighting":
return self.create_lighting_representation()
def create_plan_representation(self):
if self.settings["context"].ContextIdentifier == "Annotation":
@@ -116,7 +137,7 @@ class Usecase:
elif self.settings["context"].ContextIdentifier == "CoG":
pass
elif self.settings["context"].ContextIdentifier == "FootPrint":
if self.settings["context"].TargetView in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
if self.settings["context"].TargetView in ["SKETCH_VIEW", "PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
return self.create_geometric_curve_set_representation(is_2d=True)
elif self.settings["context"].ContextIdentifier == "Reference":
pass
@@ -125,11 +146,68 @@ class Usecase:
elif self.settings["context"].ContextIdentifier == "SurveyPoints":
pass
def create_lighting_representation(self):
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"LightSource",
[self.create_light_source()],
)
def create_light_source(self):
if self.settings["geometry"].type == "POINT":
return self.create_light_source_positional()
def create_light_source_positional(self):
return self.file.create_entity(
"IfcLightSourcePositional",
**{
"LightColour": self.file.createIfcColourRgb(None, *self.settings["geometry"].color),
"Position": self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
"Radius": self.convert_si_to_unit(self.settings["geometry"].shadow_soft_size),
},
)
def create_text_representation(self):
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"Annotation2D",
[self.create_text()],
)
def create_text(self):
text = self.settings["geometry"]
if text.align_y in ["TOP_BASELINE", "BOTTOM_BASELINE", "BOTTOM"]:
y = "bottom"
elif text.align_y == "CENTER":
y = "middle"
elif text.align_y == "TOP":
y = "top"
if text.align_x == "LEFT":
x = "left"
elif text.align_x == "CENTER":
x = "middle"
elif text.align_x == "RIGHT":
x = "right"
origin = self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
self.file.createIfcDirection((0.0, 0.0, 1.0)),
self.file.createIfcDirection((1.0, 0.0, 0.0)),
)
# TODO: Planar extent right now is wrong ...
return self.file.createIfcTextLiteralWithExtent(
text.body, origin, "RIGHT", self.file.createIfcPlanarExtent(1000, 1000), f"{y}-{x}"
)
def create_variable_representation(self):
if self.settings["is_wireframe"]:
return self.create_wireframe_representation()
elif self.settings["is_curve"]:
return self.create_curve_representation()
return self.create_curve3d_representation()
elif self.settings["is_point_cloud"]:
return self.create_point_cloud_representation()
elif isinstance(self.settings["geometry"], bpy.types.Camera):
@@ -142,6 +220,8 @@ class Usecase:
return self.create_arbitrary_extrusion_representation()
elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids":
return self.create_arbitrary_void_extrusion_representation()
elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage":
return self.create_material_profile_set_extrusion_representation()
return self.create_mesh_representation()
def create_camera_block_representation(self):
@@ -164,7 +244,7 @@ class Usecase:
"XLength": self.convert_si_to_unit(width),
"YLength": self.convert_si_to_unit(height),
"ZLength": self.convert_si_to_unit(self.settings["geometry"].clip_end),
}
},
)
return self.file.createIfcShapeRepresentation(
@@ -188,6 +268,14 @@ class Usecase:
self.create_curves(),
)
def create_curve2d_representation(self):
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"Curve2D",
self.create_curves(is_2d=True),
)
def create_curves(self, is_2d=False):
if isinstance(self.settings["geometry"], bpy.types.Mesh):
if self.file.schema == "IFC2X3":
@@ -223,7 +311,7 @@ class Usecase:
def create_curves_from_mesh_ifc2x3(self, is_2d=False):
curves = []
points = [
self.create_cartesian_point(v.co.x, v.co.y, v.co.z if is_2d else None)
self.create_cartesian_point(v.co.x, v.co.y, v.co.z if not is_2d else None)
for v in self.settings["geometry"].vertices
]
coord_list = [p.Coordinates for p in points]
@@ -308,6 +396,8 @@ class Usecase:
def create_arbitrary_void_extrusion_representation(self):
helper = Helper(self.file)
indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(self.settings["geometry"])
if not indices["inner_curves"]:
return self.create_arbitrary_extrusion_representation()
profile_def = helper.create_arbitrary_profile_def_with_voids(
self.settings["geometry"], indices["profile"], indices["inner_curves"]
)
@@ -319,6 +409,29 @@ class Usecase:
[item],
)
def create_material_profile_set_extrusion_representation(self):
profile_set = self.settings["profile_set_usage"].ForProfileSet
profile_def = profile_set.CompositeProfile or profile_set.MaterialProfiles[0].Profile
position = None
if self.file.schema == "IFC2X3":
position = self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
self.file.createIfcDirection((0.0, 0.0, 1.0)),
self.file.createIfcDirection((1.0, 0.0, 0.0)),
)
item = self.file.createIfcExtrudedAreaSolid(
profile_def,
position,
self.file.createIfcDirection((0.0, 0.0, 1.0)),
self.convert_si_to_unit(self.settings["blender_object"].dimensions[2]),
)
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"SweptSolid",
[item],
)
def create_mesh_representation(self):
if self.file.schema == "IFC2X3" or self.settings["should_force_faceted_brep"]:
return self.create_faceted_brep()
@@ -35,4 +35,18 @@ class Usecase:
)
)
self.settings["product"].RepresentationMaps = maps
if self.file.schema == "IFC2X3":
types = self.settings["product"].ObjectTypeOf
else:
types = self.settings["product"].Types
if types:
for element in types[0].RelatedObjects:
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", self.file, **{"representation": self.settings["representation"]}
)
ifcopenshell.api.run(
"geometry.assign_representation",
self.file,
**{"product": element, "representation": mapped_representation}
)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": self.settings["product"]})
@@ -14,11 +14,8 @@ class Data:
cls.products[product_id] = []
product = file.by_id(product_id)
representations = []
if product.is_a("IfcProduct"):
if product.Representation:
representations = product.Representation.Representations
else:
representations = []
if product.is_a("IfcProduct") and product.Representation:
representations = product.Representation.Representations
elif product.is_a("IfcTypeProduct"):
representations = [rm.MappedRepresentation for rm in product.RepresentationMaps or []]
for representation in representations:
@@ -33,6 +33,15 @@ class Usecase:
elif hasattr(self.settings["product"], "Decomposes") and self.settings["product"].Decomposes:
relating_object = self.settings["product"].Decomposes[0].RelatingObject
placement_rel_to = relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif hasattr(self.settings["product"], "VoidsElements") and self.settings["product"].VoidsElements:
relating_object = self.settings["product"].VoidsElements[0].RelatingBuildingElement
placement_rel_to = relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif hasattr(self.settings["product"], "FillsVoids") and self.settings["product"].FillsVoids:
relating_object = self.settings["product"].FillsVoids[0].RelatingOpeningElement
placement_rel_to = relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
elif hasattr(self.settings["product"], "ProjectsElements") and self.settings["product"].ProjectsElements:
relating_object = self.settings["product"].ProjectsElements[0].RelatingElement
placement_rel_to = relating_object.ObjectPlacement if hasattr(relating_object, "ObjectPlacement") else None
placement = self.file.createIfcLocalPlacement(placement_rel_to, self.get_relative_placement(placement_rel_to))
if self.settings["product"].ObjectPlacement:
@@ -14,12 +14,12 @@ class Usecase:
return self.remove_entire_representation_tree()
def remove_mapped_representation_portion_only(self):
dummy_context = self.file.create_entity("IfcRepresentationContext")
dummy_representation_map = self.file.createIfcRepresentationMap()
self.settings["representation"].ContextOfItems = dummy_context
for item in self.settings["representation"].Items:
item.MappingSource = dummy_representation_map
ifcopenshell.util.element.remove_deep(self.file, self.settings["representation"])
if len(self.file.get_inverse(item.MappingTarget)) == 1:
ifcopenshell.util.element.remove_deep(self.file, item.MappingTarget)
self.file.remove(item.MappingTarget)
self.file.remove(item)
self.file.remove(self.settings["representation"])
def remove_entire_representation_tree(self):
dummy_context = self.file.create_entity("IfcRepresentationContext")
@@ -53,4 +53,4 @@ class Usecase:
for item in mapped_representations:
self.unassign_product_representation(item["product"], item["representation"])
for representation in just_representations:
ifcopenshell.api.run("owner.remove_representation", self.file, **{"representation": representation})
ifcopenshell.api.run("geometry.remove_representation", self.file, **{"representation": representation})
@@ -8,22 +8,24 @@ class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"AxisCurve": None, # A Blender object
"axis_curve": None, # A Blender object
"grid_axis": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.settings["grid_axis"].AxisCurve:
ifcopenshell.util.element.remove_deep(self.file, self.settings["grid_axis"].AxisCurve)
existing_curve = self.settings["grid_axis"].AxisCurve
if existing_curve and len(self.file.get_inverse(existing_curve)) == 1:
ifcopenshell.util.element.remove_deep(self.file, existing_curve)
self.file.remove(existing_curve)
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
grid = [i for i in self.file.get_inverse(self.settings["grid_axis"]) if i.is_a("IfcGrid")][0]
points = [
Matrix(ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)).inverted()
@ (self.settings["AxisCurve"].matrix_world @ v.co)
for v in self.settings["AxisCurve"].data.vertices[0:2]
@ (self.settings["axis_curve"].matrix_world @ v.co)
for v in self.settings["axis_curve"].data.vertices[0:2]
]
self.settings["grid_axis"].AxisCurve = self.file.createIfcPolyline(
[
@@ -2,20 +2,20 @@ class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"AxisTag": "A",
"SameSense": True,
"UVWAxes": "UAxes", # Choose which axes
"Grid": None,
"axis_tag": "A",
"same_sense": True,
"uvw_axes": "UAxes", # Choose which axes
"grid": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
element = self.file.create_entity("IfcGridAxis", **{
"AxisTag": self.settings["AxisTag"],
"SameSense": self.settings["SameSense"]
"axis_tag": self.settings["axis_tag"],
"SameSense": self.settings["same_sense"]
})
axes = list(getattr(self.settings["Grid"], self.settings["UVWAxes"]) or [])
axes = list(getattr(self.settings["grid"], self.settings["uvw_axes"]) or [])
axes.append(element)
setattr(self.settings["Grid"], self.settings["UVWAxes"], axes)
setattr(self.settings["grid"], self.settings["uvw_axes"], axes)
return element
@@ -0,0 +1,15 @@
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"axis": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if len(self.file.get_inverse(self.settings["axis"].AxisCurve)) == 1:
ifcopenshell.util.element.remove_deep(self.file, self.settings["axis"].AxisCurve)
self.file.remove(self.settings["axis"].AxisCurve)
self.file.remove(self.settings["axis"])
@@ -4,9 +4,9 @@ import ifcopenshell
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"Name": "Unnamed"}
self.settings = {"name": "Unnamed"}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
return self.file.create_entity("IfcMaterial", **{"Name": self.settings["Name"] or "Unnamed"})
return self.file.create_entity("IfcMaterial", **{"Name": self.settings["name"] or "Unnamed"})
@@ -1,4 +1,7 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
class Usecase:
@@ -9,6 +12,9 @@ class Usecase:
self.settings[key] = value
def execute(self):
material = ifcopenshell.util.element.get_material(self.settings["product"])
if material:
ifcopenshell.api.run("material.unassign_material", self.file, product=self.settings["product"])
if self.settings["type"] == "IfcMaterial":
self.assign_ifc_material()
elif self.settings["type"] == "IfcMaterialConstituentSet":
@@ -18,14 +24,32 @@ class Usecase:
material_set = self.file.create_entity(self.settings["type"])
self.create_material_association(material_set)
elif self.settings["type"] == "IfcMaterialLayerSetUsage":
material_set = self.file.create_entity("IfcMaterialLayerSet")
element_type = ifcopenshell.util.element.get_type(self.settings["product"])
if element_type:
element_type_material = ifcopenshell.util.element.get_material(element_type)
if element_type_material and element_type_material.is_a("IfcMaterialLayerSet"):
material_set = element_type_material
else:
material_set = self.file.create_entity("IfcMaterialLayerSet")
else:
material_set = self.file.create_entity("IfcMaterialLayerSet")
material_set_usage = self.create_layer_set_usage(material_set)
self.create_material_association(material_set_usage)
elif self.settings["type"] == "IfcMaterialProfileSet":
material_set = self.file.create_entity(self.settings["type"])
self.create_material_association(material_set)
elif self.settings["type"] == "IfcMaterialProfileSetUsage":
material_set = self.file.create_entity("IfcMaterialProfileSet")
element_type = ifcopenshell.util.element.get_type(self.settings["product"])
if element_type:
element_type_material = ifcopenshell.util.element.get_material(element_type)
if element_type_material and element_type_material.is_a("IfcMaterialProfileSet"):
material_set = element_type_material
else:
material_set = self.file.create_entity("IfcMaterialProfileSet")
else:
material_set = self.file.create_entity("IfcMaterialProfileSet")
self.update_representation_profile(material_set)
material_set_usage = self.create_profile_set_usage(material_set)
self.create_material_association(material_set_usage)
elif self.settings["type"] == "IfcMaterialList":
@@ -33,6 +57,21 @@ class Usecase:
material_set.Materials = [self.settings["material"]]
self.create_material_association(material_set)
def update_representation_profile(self, material_set):
profile = material_set.CompositeProfile
if not profile and material_set.MaterialProfiles:
profile = material_set.MaterialProfiles[0].Profile
if not profile:
return
representation = ifcopenshell.util.representation.get_representation(
self.settings["product"], "Model", "Body", "MODEL_VIEW"
)
if not representation:
return
for subelement in self.file.traverse(representation):
if subelement.is_a("IfcSweptAreaSolid"):
subelement.SweptArea = profile
def create_layer_set_usage(self, material_set):
return self.file.create_entity(
"IfcMaterialLayerSetUsage",
@@ -1,3 +1,6 @@
import ifcopenshell.util.representation
class Usecase:
def __init__(self, file, **settings):
self.file = file
@@ -6,9 +9,31 @@ class Usecase:
self.settings[key] = value
def execute(self):
if (
self.settings["material_profile"].Profile
and len(self.file.get_inverse(self.settings["material_profile"].Profile)) == 1
):
self.file.remove(self.settings["material_profile"].Profile)
# TODO: handle composite profiles
old_profile = self.settings["material_profile"].Profile
self.settings["material_profile"].Profile = self.settings["profile"]
for profile_set in self.settings["material_profile"].ToMaterialProfileSet:
for inverse in self.file.get_inverse(profile_set):
if not inverse.is_a("IfcMaterialProfileSetUsage"):
continue
if self.file.schema == "IFC2X3":
for rel in self.file.get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.change_profile(element)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
self.change_profile(element)
if old_profile and len(self.file.get_inverse(old_profile)) == 0:
self.file.remove(old_profile)
def change_profile(self, element):
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
for subelement in self.file.traverse(representation):
if subelement.is_a("IfcSweptAreaSolid"):
subelement.SweptArea = self.settings["profile"]
@@ -9,6 +9,15 @@ class Usecase:
self.settings[key] = value
def execute(self):
for association in self.settings["product"].HasAssociations:
if association.is_a("IfcRelAssociatesMaterial"):
self.file.remove(association)
for rel in self.settings["product"].HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"):
if rel.RelatingMaterial.is_a("IfcMaterialLayerSetUsage") or rel.RelatingMaterial.is_a(
"IfcMaterialProfileSetUsage"
):
self.file.remove(rel.RelatingMaterial)
if len(rel.RelatedObjects) == 1:
self.file.remove(rel)
continue
related_objects = set(rel.RelatedObjects)
related_objects.remove(self.settings["product"])
rel.RelatedObjects = list(related_objects)
@@ -2,14 +2,16 @@ class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"Identification": "APTR",
"Name": "Aperture Science",
"identification": "APTR",
"name": "Aperture Science",
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.file.schema == "IFC2X3":
self.settings["Id"] = self.settings["Identification"]
del self.settings["Identification"]
return self.file.create_entity("IfcOrganization", **self.settings)
data = {"Name": self.settings["name"]}
if self.file.schema == "IFC2X3":
data["Id"] = self.settings["identification"]
else:
data["Identification"] = self.settings["identification"]
return self.file.create_entity("IfcOrganization", **data)
@@ -2,15 +2,17 @@ class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"Identification": "HSeldon",
"FamilyName": "Seldon",
"GivenName": "Hari",
"identification": "HSeldon",
"family_name": "Seldon",
"given_name": "Hari",
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.file.schema == "IFC2X3":
self.settings["Id"] = self.settings["Identification"]
del self.settings["Identification"]
return self.file.create_entity("IfcPerson", **self.settings)
data = {"FamilyName": self.settings["family_name"], "GivenName": self.settings["given_name"]}
if self.file.schema == "IFC2X3":
data["Id"] = self.settings["identification"]
else:
data["Identification"] = self.settings["identification"]
return self.file.create_entity("IfcPerson", **data)
@@ -11,6 +11,8 @@ class Usecase:
self.settings[key] = value
def execute(self):
if not hasattr(self.settings["element"], "OwnerHistory"):
return
self.settings["person"] = ifcopenshell.api.owner.settings.get_person(self.file)
self.settings["organisation"] = ifcopenshell.api.owner.settings.get_organisation(self.file)
if not self.settings["element"].OwnerHistory:
@@ -5,12 +5,22 @@ import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"element": None}
self.settings = {"library": None, "element": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
element = self.file.add(self.settings["element"])
self.added_elements = set()
if self.settings["element"].is_a("IfcTypeProduct"):
return self.append_type_product()
def append_type_product(self):
self.whitelisted_inverse_attributes = {
"IfcObjectDefinition": ["HasAssociations"],
"IfcMaterialDefinition": ["HasExternalReferences", "HasProperties"],
"IfcRepresentationItem": ["StyledByItem"],
}
element = self.add_element(self.settings["element"])
self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext")
added_contexts = [e for e in self.file.traverse(element) if e.is_a("IfcGeometricRepresentationContext")]
for added_context in added_contexts:
@@ -24,6 +34,24 @@ class Usecase:
ifcopenshell.util.element.remove_deep(self.file, added_context)
return element
def add_element(self, element):
if element.id() == 0 or element.id() in self.added_elements:
return
new = self.file.add(element)
self.added_elements.add(element.id())
self.add_inverse(element)
for subelement in self.settings["library"].traverse(element):
self.add_inverse(subelement)
return new
def add_inverse(self, element):
inverse_attributes = []
[inverse_attributes.extend(v) for k, v in self.whitelisted_inverse_attributes.items() if element.is_a(k)]
inverse_attributes = set(inverse_attributes)
for attribute in inverse_attributes:
for inverse in getattr(element, attribute, []):
self.add_element(inverse)
def get_equivalent_existing_context(self, added_context):
for context in self.existing_contexts:
if context.is_a() != added_context.is_a():
@@ -22,4 +22,5 @@ class Usecase:
self.file.wrapped_data.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.wrapped_data.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.wrapped_data.header.file_name.authorization = "Nobody"
self.file.wrapped_data.header.file_description.description = ('ViewDefinition[DesignTransferView]',)
return self.file
@@ -4,36 +4,53 @@ import ifcopenshell
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"product": None, "Name": None}
self.settings = {"product": None, "name": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.settings["product"].is_a("IfcObject"):
for rel in self.settings["product"].IsDefinedBy or []:
if (
rel.is_a("IfcRelDefinesByProperties")
and rel.RelatingPropertyDefinition.Name == self.settings["name"]
):
return rel.RelatingPropertyDefinition
pset = self.file.create_entity(
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]}
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["name"]}
)
self.file.create_entity(
"IfcRelDefinesByProperties",
**{
"GlobalId": ifcopenshell.guid.new(),
# TODO: owner history
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["product"]],
"RelatingPropertyDefinition": pset,
}
)
return pset
elif self.settings["product"].is_a("IfcTypeObject"):
for definition in self.settings["product"].HasPropertySets or []:
if definition.Name == self.settings["name"]:
return definition
pset = self.file.create_entity(
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]}
"IfcPropertySet", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["name"]}
)
has_property_sets = list(self.settings["product"].HasPropertySets or [])
has_property_sets.append(pset)
self.settings["product"].HasPropertySets = has_property_sets
return pset
elif self.settings["product"].is_a("IfcMaterialDefinition"):
pset = self.file.create_entity(
for definition in self.settings["product"].HasProperties or []:
if definition.Name == self.settings["name"]:
return definition
return self.file.create_entity(
"IfcMaterialProperties",
**{
"Name": self.settings["Name"],
"Name": self.settings["name"],
"Material": self.settings["product"],
}
)
@@ -1,24 +1,33 @@
import ifcopenshell
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"product": None, "Name": None}
self.settings = {"product": None, "name": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.settings["product"].is_a("IfcObject"):
for rel in self.settings["product"].IsDefinedBy or []:
if (
rel.is_a("IfcRelDefinesByProperties")
and rel.RelatingPropertyDefinition.Name == self.settings["name"]
):
return rel.RelatingPropertyDefinition
qto = self.file.create_entity(
"IfcElementQuantity", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["Name"]}
"IfcElementQuantity", **{"GlobalId": ifcopenshell.guid.new(), "Name": self.settings["name"]}
)
self.file.create_entity(
"IfcRelDefinesByProperties",
**{
"GlobalId": ifcopenshell.guid.new(),
# TODO: owner history
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["product"]],
"RelatingPropertyDefinition": qto,
}
)
return qto
@@ -7,18 +7,17 @@ class Data:
products = {}
psets = {}
qtos = {}
properties = {}
@classmethod
def purge(cls):
cls.products = {}
cls.psets = {}
cls.qtos = {}
cls.properties = {}
@classmethod
def load(cls, file, product_id):
cls._file = file
cls._psetqto = ifcopenshell.util.pset.get_template("IFC4")
cls._schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema)
if not file:
return
product = file.by_id(product_id)
@@ -59,133 +58,48 @@ class Data:
@classmethod
def add_pset(cls, pset, product_id):
new_pset = {
"Name": pset.Name,
"is_expanded": True,
"Properties": cls.get_properties_from_template(pset.Name) or [],
}
cls.products[product_id]["psets"].add(int(pset.id()))
cls.psets[int(pset.id())] = new_pset
try:
if hasattr(pset, "HasProperties"):
props = pset.HasProperties
elif hasattr(pset, "Properties"): # For IfcMaterialProperties
props = pset.Properties
except:
return # I've seen ArchiCAD produce invalid IFCs with empty data
# Invalid IFC, but some vendors like Solidworks do this so we accomodate it
if not props:
return
data = pset.get_info()
if not pset.is_a("IfcMaterialProperties"):
del data["OwnerHistory"]
del data["HasProperties"]
if hasattr(pset, "HasProperties"):
props = pset.HasProperties or []
elif hasattr(pset, "Properties"):
props = pset.Properties or []
# TODO: support more than single values
data["Properties"] = [p.id() for p in props if p.is_a("IfcPropertySingleValue")]
cls.psets[pset.id()] = data
cls.products[product_id]["psets"].add(pset.id())
for prop in props:
if prop.is_a("IfcPropertySingleValue") and prop.NominalValue:
has_existing_prop = False
for existing_prop in new_pset["Properties"]:
if existing_prop["Name"] == prop.Name:
if prop.NominalValue is None:
existing_prop["value"] = None
elif existing_prop["type"] == "string":
existing_prop["value"] = str(prop.NominalValue.wrappedValue)
elif existing_prop["type"] == "float":
existing_prop["value"] = float(prop.NominalValue.wrappedValue)
elif existing_prop["type"] == "integer":
existing_prop["value"] = int(prop.NominalValue.wrappedValue)
elif existing_prop["type"] == "boolean":
existing_prop["value"] = bool(prop.NominalValue.wrappedValue)
elif existing_prop["type"] == "enum":
existing_prop["value"] = str(prop.NominalValue.wrappedValue)
existing_prop["is_null"] = prop.NominalValue.wrappedValue is None
has_existing_prop = True
break
if not has_existing_prop:
value = prop.NominalValue.wrappedValue
if isinstance(value, str):
data_type = "string"
elif isinstance(value, float):
data_type = "float"
elif isinstance(value, bool):
data_type = "boolean"
elif isinstance(value, int):
data_type = "integer"
else:
data_type = "string"
value = str(value)
new_pset["Properties"].append(
{
"Name": prop.Name,
"value": value,
"type": data_type,
"enum_items": [],
"is_null": prop.NominalValue.wrappedValue is None,
}
)
# TODO: support more than single values
if prop.is_a("IfcPropertySingleValue"):
cls.load_prop(prop)
@classmethod
def load_prop(cls, prop):
data = prop.get_info()
if prop.is_a("IfcProperty"):
# TODO: support units
del data["Unit"]
if prop.NominalValue is not None:
data["NominalValue"] = prop.NominalValue.wrappedValue
elif prop.is_a("IfcPhysicalQuantity"):
# TODO: support units
del data["Unit"]
# For convenience, which trumps correctness in this case
data["NominalValue"] = prop[3]
cls.properties[prop.id()] = data
@classmethod
def add_qto(cls, qto, product_id):
new_qto = {
"Name": qto.Name,
"is_expanded": True,
"Properties": cls.get_properties_from_template(qto.Name) or [],
}
cls.products[product_id]["qtos"].add(int(qto.id()))
cls.qtos[int(qto.id())] = new_qto
for prop in qto.Quantities or []:
if prop.is_a("IfcPhysicalSimpleQuantity"):
value = prop[3]
has_existing_prop = False
for existing_prop in new_qto["Properties"]:
if existing_prop["Name"] == prop.Name:
existing_prop["value"] = float(value)
existing_prop["is_null"] = value is None
has_existing_prop = True
break
if not has_existing_prop:
new_qto["Properties"].append(
{
"Name": prop.Name,
"value": float(value),
"type": "float",
"enum_items": [],
"is_null": value is None,
}
)
@classmethod
def get_properties_from_template(cls, name):
template = cls._psetqto.get_by_name(name)
if not template:
return
properties = []
for prop_template in template.HasPropertyTemplates:
if not prop_template.is_a("IfcSimplePropertyTemplate"):
continue # Other types not yet supported
enum_items = []
if prop_template.TemplateType == "P_SINGLEVALUE":
try:
data_type = ifcopenshell.util.attribute.get_primitive_type(
cls._schema.declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
)
except:
# TODO: Occurs if the data type is something that exists in IFC4 and not in IFC2X3. To fully fix
# this we need to generate the IFC2X3 pset template definitions.
continue
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
data_type = "enum"
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
elif prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
data_type = "float"
elif prop_template.TemplateType == "Q_COUNT":
data_type = "integer"
else:
continue # Other types not yet supported
properties.append(
{
"Name": prop_template.Name,
"value": None,
"type": data_type,
"enum_items": enum_items,
"is_null": True,
}
)
return properties
data = qto.get_info()
del data["OwnerHistory"]
del data["Quantities"]
# TODO: support more than just simple quantities
# We call it properties for convenience, not for correctness
data["Properties"] = [q.id() for q in qto.Quantities or [] if q.is_a("IfcPhysicalSimpleQuantity")]
cls.qtos[qto.id()] = data
cls.products[product_id]["qtos"].add(qto.id())
for quantity in qto.Quantities or []:
if quantity.is_a("IfcPhysicalSimpleQuantity"):
cls.load_prop(quantity)
@@ -1,10 +1,11 @@
import ifcopenshell
import ifcopenshell.util.pset
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"pset": None, "Name": None, "Properties": {}}
self.settings = {"pset": None, "name": None, "properties": {}}
for key, value in settings.items():
self.settings[key] = value
@@ -16,8 +17,8 @@ class Usecase:
self.extend_pset_with_new_properties(new_properties)
def update_pset_name(self):
if self.settings["Name"]:
self.settings["pset"].Name = self.settings["Name"]
if self.settings["name"]:
self.settings["pset"].Name = self.settings["name"]
def load_pset_template(self):
# TODO: add IFC2X3 PsetQto template support
@@ -29,19 +30,19 @@ class Usecase:
self.update_existing_property(prop)
def update_existing_property(self, prop):
if prop.Name not in self.settings["Properties"]:
if prop.Name not in self.settings["properties"]:
return
value = self.settings["Properties"][prop.Name]
value = self.settings["properties"][prop.Name]
if value is None:
prop.NominalValue = None
else:
primary_measure_type = self.get_primary_measure_type(prop.Name, previous_value=prop.NominalValue)
prop.NominalValue = self.file.create_entity(primary_measure_type, value)
del self.settings["Properties"][prop.Name]
del self.settings["properties"][prop.Name]
def add_new_properties(self):
properties = []
for name, value in self.settings["Properties"].items():
for name, value in self.settings["properties"].items():
if value is None:
continue
primary_measure_type = self.get_primary_measure_type(name)
@@ -4,7 +4,7 @@ import ifcopenshell
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"qto": None, "Name": None, "Properties": {}}
self.settings = {"qto": None, "name": None, "properties": {}}
for key, value in settings.items():
self.settings[key] = value
@@ -16,8 +16,8 @@ class Usecase:
self.extend_qto_with_new_properties(new_properties)
def update_qto_name(self):
if self.settings["Name"]:
self.settings["qto"].Name = self.settings["Name"]
if self.settings["name"]:
self.settings["qto"].Name = self.settings["name"]
def load_qto_template(self):
# TODO: add IFC2X3 PsetQto template support
@@ -29,16 +29,19 @@ class Usecase:
self.update_existing_property(prop)
def update_existing_property(self, prop):
if prop.Name not in self.settings["Properties"]:
if prop.Name not in self.settings["properties"]:
return
value = self.settings["Properties"][prop.Name]
if prop.is_a("IfcPhysicalSimpleQuantity"):
prop[3] = float(value) if value else None
del self.settings["Properties"][prop.Name]
value = self.settings["properties"][prop.Name]
name = prop.Name
if value is None:
self.file.remove(prop)
elif prop.is_a("IfcPhysicalSimpleQuantity"):
prop[3] = float(value)
del self.settings["properties"][name]
def add_new_properties(self):
properties = []
for name, value in self.settings["Properties"].items():
for name, value in self.settings["properties"].items():
if value is None:
continue
property_type = self.get_canonical_property_type(name)
@@ -18,9 +18,20 @@ class Usecase:
elif self.settings["product"].is_a("IfcTypeProduct"):
representations = [rm.MappedRepresentation for rm in self.settings["product"].RepresentationMaps or []]
for representation in representations:
ifcopenshell.api.run("geometry.unassign_representation",
self.file, **{"product": self.settings["product"], "representation": representation}
ifcopenshell.api.run(
"geometry.unassign_representation",
self.file,
**{"product": self.settings["product"], "representation": representation}
)
ifcopenshell.api.run("geometry.remove_representation", self.file, **{"representation": representation})
for opening in getattr(self.settings["product"], "HasOpenings", []) or []:
ifcopenshell.api.run("void.remove_opening", self.file, opening=opening.RelatedOpeningElement)
if self.settings["product"].is_a("IfcGrid"):
for axis in (
self.settings["product"].UAxes + self.settings["product"].VAxes + (self.settings["product"].WAxes or ())
):
ifcopenshell.api.run("grid.remove_grid_axis", self.file, axis=axis)
# TODO: remove object placement and other relationships
self.file.remove(self.settings["product"])
@@ -1,6 +1,4 @@
import ifcopenshell.util.date
from datetime import datetime
from datetime import timedelta
class Usecase:
def __init__(self, file, **settings):
@@ -4,7 +4,7 @@ import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"name": "Unnamed", "predefined_type": "NOTDEFINED", "working_times": [], "exception_times": []}
self.settings = {"name": "Unnamed", "predefined_type": "NOTDEFINED"}
for key, value in settings.items():
self.settings[key] = value
@@ -0,0 +1,28 @@
import ifcopenshell.util.date
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"rel_sequence": None, "lag_value": None, "duration_type": "NOTDEFINED"}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
lag_value = self.file.createIfcDuration(
ifcopenshell.util.date.datetime2ifc(self.settings["lag_value"], "IfcDuration")
)
lag_time = self.file.create_entity(
"IfcLagTime",
**{
"DurationType": self.settings["duration_type"],
"LagValue": lag_value,
}
)
if self.settings["rel_sequence"].is_a("IfcRelSequence"):
if (
self.settings["rel_sequence"].TimeLag
and len(self.file.get_inverse(self.settings["rel_sequence"].TimeLag)) == 1
):
self.file.remove(self.settings["rel_sequence"].TimeLag)
self.settings["rel_sequence"].TimeLag = lag_time
@@ -0,0 +1,43 @@
import ifcopenshell
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"relating_process": None,
"related_object": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.settings["related_object"].HasAssignments:
for assignment in self.settings["related_object"].HasAssignments:
if (
assignment.is_a("IfclRelAssignsToProcess")
and assignment.RelatingProcess == self.settings["relating_process"]
):
return
operates_on = None
if self.settings["relating_process"].OperatesOn:
operates_on = self.settings["relating_process"].OperatesOn[0]
if operates_on:
related_objects = list(operates_on.RelatedObjects)
related_objects.append(self.settings["related_object"])
operates_on.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": operates_on})
else:
operates_on = self.file.create_entity(
"IfcRelAssignsToProcess",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["related_object"]],
"RelatingProcess": self.settings["relating_process"],
}
)
return operates_on
@@ -19,3 +19,4 @@ class Usecase:
if len(self.file.get_inverse(self.settings["parent"].Recurrence)) == 1:
self.file.remove(self.settings["parent"].Recurrence)
self.settings["parent"].Recurrence = recurrence
return recurrence
@@ -23,5 +23,6 @@ class Usecase:
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatingProcess": self.settings["relating_process"],
"RelatedProcess": self.settings["related_process"],
"SequenceType": "FINISH_START",
}
)
@@ -0,0 +1,161 @@
import datetime
import ifcopenshell.util.date
import ifcopenshell.util.sequence
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"task": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.calendar_cache = {}
self.cascade_task(self.settings["task"])
def cascade_task(self, task):
if not task.TaskTime:
return
duration = (
ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration)
if task.TaskTime.ScheduleDuration
else datetime.timedelta()
)
finishes = []
starts = []
for rel in task.IsSuccessorFrom:
predecessor = rel.RelatingProcess
if rel.SequenceType == "FINISH_START":
finish = self.get_task_time_attribute(predecessor, "ScheduleFinish")
if not finish:
continue
if rel.TimeLag:
days = self.get_lag_time_days(rel.TimeLag)
duration_type = rel.TimeLag.DurationType
starts.append(self.offset_date(finish, days, duration_type, self.get_calendar(task)))
starts.append(self.offset_date(finish, days, duration_type, self.get_calendar(predecessor)))
else:
starts.append(finish)
elif rel.SequenceType == "START_START":
start = self.get_task_time_attribute(predecessor, "ScheduleStart")
if not start:
continue
if rel.TimeLag:
days = self.get_lag_time_days(rel.TimeLag)
duration_type = rel.TimeLag.DurationType
starts.append(self.offset_date(start, days, duration_type, self.get_calendar(task)))
starts.append(self.offset_date(start, days, duration_type, self.get_calendar(predecessor)))
else:
starts.append(start)
elif rel.SequenceType == "FINISH_FINISH":
finish = self.get_task_time_attribute(predecessor, "ScheduleFinish")
if not finish:
continue
if rel.TimeLag:
days = self.get_lag_time_days(rel.TimeLag)
duration_type = rel.TimeLag.DurationType
finishes.append(self.offset_date(finish, days, duration_type, self.get_calendar(task)))
finishes.append(self.offset_date(finish, days, duration_type, self.get_calendar(predecessor)))
else:
finishes.append(finish)
elif rel.SequenceType == "START_FINISH":
start = self.get_task_time_attribute(predecessor, "ScheduleStart")
if not start:
continue
if rel.TimeLag:
days = self.get_lag_time_days(rel.TimeLag)
duration_type = rel.TimeLag.DurationType
finishes.append(self.offset_date(start, days, duration_type, self.get_calendar(task)))
finishes.append(self.offset_date(start, days, duration_type, self.get_calendar(predecessor)))
else:
finishes.append(start)
if starts and finishes:
start = max(starts)
finish = max(finishes)
potential_finish = datetime.datetime.combine(
ifcopenshell.util.sequence.get_finish_date(
start,
duration,
task.TaskTime.DurationType,
self.get_calendar(task),
),
datetime.datetime.min.time(),
)
if potential_finish > finish:
start_ifc = ifcopenshell.util.date.datetime2ifc(start, "IfcDateTime")
if task.TaskTime.ScheduleStart == start_ifc:
return
task.TaskTime.ScheduleStart = start_ifc
task.TaskTime.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(potential_finish, "IfcDateTime")
else:
finish_ifc = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
if task.TaskTime.ScheduleFinish == finish_ifc:
return
task.TaskTime.ScheduleFinish = finish_ifc
task.TaskTime.ScheduleStart = ifcopenshell.util.date.datetime2ifc(
ifcopenshell.util.sequence.get_finish_date(
finish,
-duration,
task.TaskTime.DurationType,
self.get_calendar(task),
),
"IfcDateTime",
)
elif finishes:
finish = max(finishes)
finish_ifc = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
if task.TaskTime.ScheduleFinish == finish_ifc:
return
task.TaskTime.ScheduleFinish = finish_ifc
task.TaskTime.ScheduleStart = ifcopenshell.util.date.datetime2ifc(
ifcopenshell.util.sequence.get_finish_date(
finish,
-duration,
task.TaskTime.DurationType,
self.get_calendar(task),
),
"IfcDateTime",
)
elif starts:
start = max(starts)
start_ifc = ifcopenshell.util.date.datetime2ifc(start, "IfcDateTime")
if task.TaskTime.ScheduleStart == start_ifc:
return
task.TaskTime.ScheduleStart = start_ifc
task.TaskTime.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(
ifcopenshell.util.sequence.get_finish_date(
start,
duration,
task.TaskTime.DurationType,
self.get_calendar(task),
),
"IfcDateTime",
)
for rel in task.IsPredecessorTo:
self.cascade_task(rel.RelatedProcess)
def get_lag_time_days(self, lag_time):
return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days
def get_calendar(self, task):
if task.id() not in self.calendar_cache:
self.calendar_cache[task.id()] = ifcopenshell.util.sequence.derive_calendar(task)
return self.calendar_cache[task.id()]
def offset_date(self, date, days, duration_type, calendar):
return datetime.datetime.combine(
ifcopenshell.util.sequence.get_finish_date(date, datetime.timedelta(days=days), duration_type, calendar),
datetime.datetime.min.time(),
)
def get_task_time_attribute(self, task, attribute):
if task.TaskTime:
value = getattr(task.TaskTime, attribute)
if value:
return ifcopenshell.util.date.ifc2datetime(value)
@@ -11,6 +11,8 @@ class Data:
time_periods = {}
tasks = {}
task_times = {}
lag_times = {}
sequences = {}
@classmethod
def purge(cls):
@@ -23,6 +25,8 @@ class Data:
cls.time_periods = {}
cls.tasks = {}
cls.task_times = {}
cls.lag_times = {}
cls.sequences = {}
@classmethod
def load(cls, file):
@@ -37,6 +41,8 @@ class Data:
cls.load_time_periods()
cls.load_tasks()
cls.load_task_times()
cls.load_lag_times()
cls.load_sequences()
cls.is_loaded = True
@classmethod
@@ -118,21 +124,30 @@ class Data:
for task in cls._file.by_type("IfcTask"):
data = task.get_info()
del data["OwnerHistory"]
data["HasAssignmentsWorkCalendar"] = []
data["RelatedObjects"] = []
data["RelatingProducts"] = []
data["OperatesOn"] = []
data["IsPredecessorTo"] = []
data["IsSuccessorFrom"] = []
if task.TaskTime:
data["TaskTime"] = data["TaskTime"].id()
for rel in task.IsNestedBy:
[data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcTask")]
data["Nests"] = [r.RelatingObject.id() for r in task.Nests or []]
[
data["RelatingProducts"].append(r.RelatingProduct.id())
for r in task.HasAssignments
if r.is_a("IfcRelAssignsToProduct")
]
[data["IsPredecessorTo"].append(rel.RelatedProcess.id()) for rel in task.IsPredecessorTo or []]
[data["IsSuccessorFrom"].append(rel.RelatingProcess.id()) for rel in task.IsSuccessorFrom or []]
[data["OperatesOn"].extend([o.id() for o in r.RelatedObjects]) for r in task.OperatesOn]
[data["IsPredecessorTo"].append(rel.id()) for rel in task.IsPredecessorTo or []]
[data["IsSuccessorFrom"].append(rel.id()) for rel in task.IsSuccessorFrom or []]
[
data["HasAssignmentsWorkCalendar"].append(rel.RelatingControl.id())
for rel in task.HasAssignments or []
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar")
]
cls.tasks[task.id()] = data
@classmethod
@@ -145,5 +160,28 @@ class Data:
continue
if "Start" in key or "Finish" in key or key == "StatusTime":
data[key] = ifcopenshell.util.date.ifc2datetime(value)
# TODO parse duration
elif key == "ScheduleDuration":
data[key] = ifcopenshell.util.date.ifc2datetime(value)
cls.task_times[task_time.id()] = data
@classmethod
def load_lag_times(cls):
cls.lag_times = {}
for lag_time in cls._file.by_type("IfcLagTime"):
data = lag_time.get_info()
if data["LagValue"]:
if data["LagValue"].is_a("IfcDuration"):
data["LagValue"] = ifcopenshell.util.date.ifc2datetime(data["LagValue"].wrappedValue)
else:
data["LagValue"] = float(data["LagValue"].wrappedValue)
cls.lag_times[lag_time.id()] = data
@classmethod
def load_sequences(cls):
cls.sequences = {}
for sequence in cls._file.by_type("IfcRelSequence"):
data = sequence.get_info()
data["RelatingProcess"] = sequence.RelatingProcess.id()
data["RelatedProcess"] = sequence.RelatedProcess.id()
data["TimeLag"] = sequence.TimeLag.id() if sequence.TimeLag else None
cls.sequences[sequence.id()] = data
@@ -0,0 +1,21 @@
import ifcopenshell.api
import ifcopenshell.util.date
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"lag_time": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
if name == "LagValue" and value is not None:
if isinstance(value, float):
value = self.file.createIfcRatioMeasure(value)
else:
value = self.file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(value, "IfcDuration"))
setattr(self.settings["lag_time"], name, value)
for rel in [r for r in self.file.get_inverse(self.settings["lag_time"]) if r.is_a("IfcRelSequence")]:
ifcopenshell.api.run("sequence.cascade_schedule", self.file, task=rel.RelatedProcess)
@@ -1,6 +1,3 @@
import ifcopenshell.util.date
class Usecase:
def __init__(self, file, **settings):
self.file = file
@@ -10,17 +7,4 @@ class Usecase:
def execute(self):
for name, value in self.settings["attributes"].items():
if name == "TimePeriods" and value:
periods = []
for period in value:
periods.append(
self.file.create_entity(
"IfcTimePeriod",
**{
"StartTime": ifcopenshell.util.date.datetime2ifc(period[0]),
"EndTime": ifcopenshell.util.date.datetime2ifc(period[1]),
},
)
)
value = periods
setattr(self.settings["recurrence_pattern"], name, value)
@@ -0,0 +1,18 @@
import ifcopenshell
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"rel_sequence": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["rel_sequence"], name, value)
if "SequenceType" in self.settings["attributes"].keys():
ifcopenshell.api.run(
"sequence.cascade_schedule", self.file, task=self.settings["rel_sequence"].RelatedProcess
)
@@ -1,4 +1,6 @@
import datetime
import ifcopenshell.util.date
import ifcopenshell.util.sequence
class Usecase:
@@ -9,8 +11,82 @@ class Usecase:
self.settings[key] = value
def execute(self):
self.task = self.get_task()
self.calendar = ifcopenshell.util.sequence.derive_calendar(self.task)
# If the user specifies both an end date and a duration, the duration takes priority
if (
"ScheduleDuration" in self.settings["attributes"].keys()
and "ScheduleFinish" in self.settings["attributes"].keys()
):
del self.settings["attributes"]["ScheduleFinish"]
if (
"ActualDuration" in self.settings["attributes"].keys()
and "ActualFinish" in self.settings["attributes"].keys()
):
del self.settings["attributes"]["ActualFinish"]
duration_type = self.settings["attributes"].get("DurationType", self.settings["task_time"].DurationType)
if "ScheduleFinish" in self.settings["attributes"]:
self.settings["attributes"]["ScheduleFinish"] = ifcopenshell.util.sequence.get_soonest_working_day(
self.settings["attributes"]["ScheduleFinish"], duration_type, self.calendar
)
if "ScheduleStart" in self.settings["attributes"]:
self.settings["attributes"]["ScheduleStart"] = ifcopenshell.util.sequence.get_soonest_working_day(
self.settings["attributes"]["ScheduleStart"], duration_type, self.calendar
)
for name, value in self.settings["attributes"].items():
if "Start" in name or "Finish" in name or name == "StatusTime":
if value:
if value:
if "Start" in name or "Finish" in name or name == "StatusTime":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "ScheduleDuration" or name == "ActualDuration" or name == "RemainingTime":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
setattr(self.settings["task_time"], name, value)
if (
"ScheduleDuration" in self.settings["attributes"].keys()
and self.settings["task_time"].ScheduleDuration
and self.settings["task_time"].ScheduleStart
):
self.calculate_finish()
elif "ScheduleStart" in self.settings["attributes"].keys() and self.settings["task_time"].ScheduleDuration:
self.calculate_finish()
elif "ScheduleFinish" in self.settings["attributes"].keys() and self.settings["task_time"].ScheduleStart:
self.calculate_duration()
if (
self.settings["task_time"].ScheduleDuration
and (
"ScheduleStart" in self.settings["attributes"].keys()
or "ScheduleFinish" in self.settings["attributes"].keys()
or "ScheduleDuration" in self.settings["attributes"].keys()
)
):
ifcopenshell.api.run("sequence.cascade_schedule", self.file, task=self.task)
def calculate_finish(self):
finish_date = ifcopenshell.util.sequence.get_finish_date(
ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart),
ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleDuration),
self.settings["task_time"].DurationType,
self.calendar,
)
self.settings["task_time"].ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish_date, "IfcDateTime")
def calculate_duration(self):
start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart)
finish = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleFinish)
current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day)
duration = datetime.timedelta()
while current_date < finish_date:
if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not self.calendar:
duration += datetime.timedelta(days=1)
elif ifcopenshell.util.sequence.is_working_day(current_date, self.calendar):
duration += datetime.timedelta(days=1)
current_date += datetime.timedelta(days=1)
self.settings["task_time"].ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration")
def get_task(self):
return [e for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask")][0]
@@ -1,3 +1,6 @@
import ifcopenshell.util.date
class Usecase:
def __init__(self, file, **settings):
self.file = file
@@ -7,4 +10,9 @@ class Usecase:
def execute(self):
for name, value in self.settings["attributes"].items():
if value:
if "Date" in name or "Time" in name:
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "Duration" or name == "TotalFloat":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
setattr(self.settings["work_plan"], name, value)
@@ -1,3 +1,6 @@
import ifcopenshell.util.date
class Usecase:
def __init__(self, file, **settings):
self.file = file
@@ -7,4 +10,9 @@ class Usecase:
def execute(self):
for name, value in self.settings["attributes"].items():
if value:
if "Date" in name or "Time" in name:
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "Duration" or name == "TotalFloat":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
setattr(self.settings["work_schedule"], name, value)
@@ -0,0 +1,28 @@
import ifcopenshell
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"relating_product": None,
"related_object": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
products = set()
related_object = None
if self.settings["related_object"]:
related_object = self.settings["related_object"]
elif self.settings["relating_product"]:
for reference in self.settings["relating_product"].ReferencedBy:
if reference.is_a("IfcRelAssignsToProduct"):
related_object = referenced_by.RelatedObjects[0]
if related_object:
assignments = self.settings["related_object"].HasAssignments
for assignment in assignments:
if assignment.is_a("IfcRelAssignsToProduct"):
products.add(assignment.RelatingProduct.id())
return products
@@ -0,0 +1,320 @@
import datetime
import networkx as nx
import ifcopenshell.api
import ifcopenshell.util.date
import ifcopenshell.util.sequence
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"work_schedule": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
# The method implemented is the same as shown here:
# https://www.youtube.com/watch?v=qTErIV6OqLg
self.start_dates = []
self.build_network_graph()
self.pending_nodes = set(self.g.nodes)
while self.pending_nodes:
remaining_nodes = set()
for pending_node in self.pending_nodes:
if not self.forward_pass(pending_node):
remaining_nodes.add(pending_node)
self.pending_nodes = remaining_nodes
self.pending_nodes = set(self.g.nodes)
while self.pending_nodes:
remaining_nodes = set()
for pending_node in self.pending_nodes:
if not self.backward_pass(pending_node):
remaining_nodes.add(pending_node)
self.pending_nodes = remaining_nodes
self.update_task_times()
def build_network_graph(self):
self.sequence_type_map = {
None: "FS",
"START_START": "SS",
"START_FINISH": "SF",
"FINISH_START": "FS",
"FINISH_FINISH": "FF",
"USERDEFINED": "FS",
"NOTDEFINED": "FS",
}
self.g = nx.DiGraph()
self.edges = []
self.g.add_node("start", duration=0, duration_type="ELAPSEDTIME", calendar=None)
self.g.add_node("finish", duration=0, duration_type="ELAPSEDTIME", calendar=None)
for rel in self.settings["work_schedule"].Controls:
for related_object in rel.RelatedObjects:
if not related_object.is_a("IfcTask"):
continue
self.add_node(related_object)
self.g.add_edges_from(self.edges)
def add_node(self, task):
if task.IsNestedBy:
for rel in task.IsNestedBy:
[self.add_node(o) for o in rel.RelatedObjects]
return
if task.TaskTime and task.TaskTime.ScheduleDuration:
duration = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration).days
duration_type = task.TaskTime.DurationType
else:
duration = 0
duration_type = "ELAPSEDTIME"
self.g.add_node(
task.id(),
duration=duration,
duration_type=duration_type,
calendar=ifcopenshell.util.sequence.derive_calendar(task),
)
self.edges.extend(
[
(
rel.RelatingProcess.id(),
rel.RelatedProcess.id(),
{
"lag_time": 0
if not rel.TimeLag
else ifcopenshell.util.date.ifc2datetime(rel.TimeLag.LagValue.wrappedValue).days,
"type": self.sequence_type_map[rel.SequenceType],
},
)
for rel in task.IsSuccessorFrom or []
]
)
predecessor_types = [rel.SequenceType for rel in task.IsSuccessorFrom]
successor_types = [rel.SequenceType for rel in task.IsPredecessorTo]
if not predecessor_types or (
"FINISH_START" not in predecessor_types and "START_START" not in predecessor_types
):
self.edges.append(("start", task.id(), {"lag_time": 0, "type": "FS"}))
if task.TaskTime and task.TaskTime.ScheduleStart:
self.start_dates.append(ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleStart))
if not successor_types or ("FINISH_START" not in successor_types and "FINISH_FINISH" not in successor_types):
self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FS"}))
def update_task_times(self):
for ifc_definition_id in self.g.nodes:
data = self.g.nodes[ifc_definition_id]
if not data["duration"]:
continue
ifcopenshell.api.run(
"sequence.edit_task_time",
self.file,
task_time=self.file.by_id(ifc_definition_id).TaskTime,
attributes={
"FreeFloat": ifcopenshell.util.date.datetime2ifc(data["free_float"], "IfcDuration"),
"TotalFloat": ifcopenshell.util.date.datetime2ifc(data["total_float"], "IfcDuration"),
"IsCritical": data["total_float"].days == 0,
"EarlyStart": ifcopenshell.util.date.datetime2ifc(data["early_start"], "IfcDateTime"),
"EarlyFinish": ifcopenshell.util.date.datetime2ifc(data["early_finish"], "IfcDateTime"),
"LateStart": ifcopenshell.util.date.datetime2ifc(data["late_start"], "IfcDateTime"),
"LateFinish": ifcopenshell.util.date.datetime2ifc(data["late_finish"], "IfcDateTime"),
},
)
def offset_date(self, date, days, node):
return ifcopenshell.util.sequence.get_finish_date(
date, datetime.timedelta(days=days), node["duration_type"], node["calendar"]
)
def forward_pass(self, node):
successors = self.g.successors(node)
predecessors = list(self.g.predecessors(node))
data = self.g.nodes[node]
if node == "start":
data["early_start"] = min(self.start_dates)
else:
finishes = []
starts = []
for predecessor in predecessors:
predecessor_data = self.g.nodes[predecessor]
edge = self.g[predecessor][node]
if edge["type"] == "FS":
finish = predecessor_data.get("early_finish")
if finish is None:
return
if not edge["lag_time"]:
starts.append(finish)
else:
starts.append(self.offset_date(finish, edge["lag_time"], data))
starts.append(self.offset_date(finish, edge["lag_time"], predecessor_data))
elif edge["type"] == "SS":
start = predecessor_data.get("early_start")
if start is None:
return
if not edge["lag_time"]:
starts.append(start)
else:
starts.append(self.offset_date(start, edge["lag_time"], data))
starts.append(self.offset_date(start, edge["lag_time"], predecessor_data))
elif edge["type"] == "FF":
finish = predecessor_data.get("early_finish")
if finish is None:
return
if not edge["lag_time"]:
finishes.append(finish)
else:
finishes.append(self.offset_date(finish, edge["lag_time"], data))
finishes.append(self.offset_date(finish, edge["lag_time"], predecessor_data))
elif edge["type"] == "SF":
start = predecessor_data.get("early_start")
if start is None:
return
if not edge["lag_time"]:
finishes.append(start)
else:
finishes.append(self.offset_date(start, edge["lag_time"], data))
finishes.append(self.offset_date(start, edge["lag_time"], predecessor_data))
if starts and finishes:
data["early_start"] = max(starts)
data["early_finish"] = max(finishes)
if self.offset_date(data["early_start"], data["duration"], data) > data["early_finish"]:
data["early_finish"] = self.offset_date(data["early_start"], data["duration"], data)
else:
data["early_start"] = self.offset_date(data["early_finish"], -data["duration"], data)
elif finishes:
data["early_finish"] = max(finishes)
elif starts:
data["early_start"] = max(starts)
else:
print("How did this happen?")
if data.get("early_finish") is None:
data["early_finish"] = self.offset_date(data["early_start"], data["duration"], data)
elif data.get("early_start") is None:
data["early_start"] = self.offset_date(data["early_finish"], -data["duration"], data)
return True
def backward_pass(self, node):
successors = list(self.g.successors(node))
predecessors = self.g.predecessors(node)
data = self.g.nodes[node]
free_floats = []
if node == "finish":
data["late_finish"] = data["early_finish"]
else:
finishes = []
starts = []
for successor in successors:
successor_data = self.g.nodes[successor]
edge = self.g[node][successor]
if edge["type"] == "FS":
start = successor_data.get("late_start")
if start is None:
return
if not edge["lag_time"]:
finishes.append(start)
else:
finishes.append(self.offset_date(start, -edge["lag_time"], data))
finishes.append(self.offset_date(start, -edge["lag_time"], successor_data))
free_floats.append(
self.calculate_free_float(
data["early_finish"], successor_data["early_start"], edge["lag_time"], data, successor_data
)
)
elif edge["type"] == "SS":
start = successor_data.get("late_start")
if start is None:
return
if not edge["lag_time"]:
starts.append(start)
else:
starts.append(self.offset_date(start, -edge["lag_time"], data))
starts.append(self.offset_date(start, -edge["lag_time"], successor_data))
free_floats.append(
self.calculate_free_float(
data["early_start"], successor_data["early_start"], edge["lag_time"], data, successor_data
)
)
elif edge["type"] == "FF":
finish = successor_data.get("late_finish")
if finish is None:
return
if not edge["lag_time"]:
finishes.append(finish)
else:
finishes.append(self.offset_date(finish, -edge["lag_time"], data))
finishes.append(self.offset_date(finish, -edge["lag_time"], successor_data))
free_floats.append(
self.calculate_free_float(
data["early_finish"], successor_data["early_finish"], edge["lag_time"], data, successor_data
)
)
elif edge["type"] == "SF":
finish = successor_data.get("late_finish")
if finish is None:
return
if not edge["lag_time"]:
starts.append(finish)
else:
starts.append(self.offset_date(finish, -edge["lag_time"], data))
starts.append(self.offset_date(finish, -edge["lag_time"], successor_data))
free_floats.append(
self.calculate_free_float(
data["early_start"], successor_data["early_finish"], edge["lag_time"], data, successor_data
)
)
if starts and finishes:
data["late_start"] = min(starts)
data["late_finish"] = min(finishes)
if self.offset_date(data["late_start"], data["duration"], data) < data["late_finish"]:
data["late_finish"] = self.offset_date(data["late_start"], data["duration"], data)
else:
data["late_start"] = self.offset_date(data["late_finish"], -data["duration"], data)
elif finishes:
data["late_finish"] = min(finishes)
elif starts:
data["late_start"] = min(starts)
else:
print("How did this happen?")
if data.get("late_finish") is None:
data["late_finish"] = self.offset_date(data["late_start"], data["duration"], data)
elif data.get("late_start") is None:
data["late_start"] = self.offset_date(data["late_finish"], -data["duration"], data)
if data["duration_type"] == "WORKTIME":
data["total_float"] = datetime.timedelta(
days=ifcopenshell.util.sequence.count_working_days(
data["early_finish"], data["late_finish"], data["calendar"]
)
)
else:
data["total_float"] = data["late_finish"] - data["early_finish"]
data["free_float"] = min(free_floats) if free_floats else None
return True
def calculate_free_float(self, predecessor_date, successor_date, lag_time, predecessor_data, successor_data):
if not lag_time:
min_successor_date = successor_date
else:
min_successor_date = min(
(
self.offset_date(successor_date, -lag_time, predecessor_data),
self.offset_date(successor_date, -lag_time, successor_data),
)
)
if predecessor_data["duration_type"] == "WORKTIME":
return datetime.timedelta(
days=ifcopenshell.util.sequence.count_working_days(
predecessor_date, min_successor_date, predecessor_data["calendar"]
)
)
return min_successor_date - predecessor_date
@@ -16,4 +16,15 @@ class Usecase:
definition=self.settings["task"],
relating_context=self.file.by_type("IfcContext")[0],
)
for inverse in self.file.get_inverse(self.settings["task"]):
if inverse.is_a("IfcRelSequence"):
self.file.remove(inverse)
elif inverse.is_a("IfcRelNests"):
if inverse.RelatingObject == self.settings["task"]:
for related_object in inverse.RelatedObjects:
ifcopenshell.api.run("sequence.remove_task", self.file, task=related_object)
elif inverse.RelatedObjects == tuple(self.settings["task"]):
self.file.remove(inverse)
elif inverse.is_a("IfcRelAssignsToControl"):
self.file.remove(inverse)
self.file.remove(self.settings["task"])
@@ -16,4 +16,12 @@ class Usecase:
definition=self.settings["work_schedule"],
relating_context=self.file.by_type("IfcContext")[0],
)
for rel in self.settings["work_schedule"].Controls:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcTask"):
ifcopenshell.api.run(
"sequence.remove_task",
self.file,
task=related_object
)
self.file.remove(self.settings["work_schedule"])
@@ -0,0 +1,20 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"rel_sequence": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if len(self.file.get_inverse(self.settings["rel_sequence"].TimeLag)) == 1:
self.file.remove(self.settings["rel_sequence"].TimeLag)
else:
self.settings["rel_sequence"].TimeLag = None
ifcopenshell.api.run(
"sequence.cascade_schedule", self.file, task=self.settings["rel_sequence"].RelatedProcess
)
@@ -0,0 +1,25 @@
import ifcopenshell
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"relating_process": None,
"related_object": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for rel in self.settings["related_object"].HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProcess") or rel.RelatingProcess != self.settings["relating_process"]:
continue
if len(rel.RelatedObjects) == 1:
return self.file.remove(rel)
related_objects = list(rel.RelatedObjects)
related_objects.remove(self.settings["related_object"])
rel.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
return rel
@@ -16,3 +16,4 @@ class Usecase:
for rel in self.settings["related_process"].IsSuccessorFrom or []:
if rel.RelatingProcess == self.settings["relating_process"]:
self.file.remove(rel)
ifcopenshell.api.run("sequence.cascade_schedule", self.file, task=self.settings["related_process"])
@@ -0,0 +1,30 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"ifc_class": "IfcStructuralPlanarAction",
"predefined_type": "CONST",
"global_or_local": "GLOBAL_COORDS",
"applied_load": None,
"structural_member": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
activity = ifcopenshell.api.run(
"root.create_entity",
self.file,
ifc_class=self.settings["ifc_class"],
predefined_type=self.settings["predefined_type"],
)
activity.AppliedLoad = self.settings["applied_load"]
activity.GlobalOrLocal = self.settings["global_or_local"]
rel = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcRelConnectsStructuralActivity")
rel.RelatingElement = self.settings["structural_member"]
rel.RelatedStructuralActivity = activity
return activity
@@ -1,21 +1,28 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"connection": None}
self.settings = {"name": None, "connection": None, "ifc_class": "IfcBoundaryNodeCondition"}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.settings["connection"].is_a("IfcRelConnectsStructuralMember"):
related_connection = self.settings["connection"].RelatedStructuralConnection
if self.settings["connection"]:
# assign boundary condition to a connection
if self.settings["connection"].is_a("IfcRelConnectsStructuralMember"):
related_connection = self.settings["connection"].RelatedStructuralConnection
else:
related_connection = self.settings["connection"]
if related_connection.is_a("IfcStructuralPointConnection"):
boundary_class = "IfcBoundaryNodeCondition"
elif related_connection.is_a("IfcStructuralCurveConnection"):
boundary_class = "IfcBoundaryEdgeCondition"
elif related_connection.is_a("IfcStructuralSurfaceConnection"):
boundary_class = "IfcBoundaryFaceCondition"
self.settings["connection"].AppliedCondition = self.file.create_entity(
boundary_class, Name=self.settings["name"]
)
else:
related_connection = self.settings["connection"]
if related_connection.is_a("IfcStructuralPointConnection"):
boundary_class = "IfcBoundaryNodeCondition"
elif related_connection.is_a("IfcStructuralCurveConnection"):
boundary_class = "IfcBoundaryEdgeCondition"
elif related_connection.is_a("IfcStructuralSurfaceConnection"):
boundary_class = "IfcBoundaryFaceCondition"
self.settings["connection"].AppliedCondition = self.file.create_entity(boundary_class)
# add an orphan boundary condition
return self.file.create_entity(self.settings["ifc_class"], Name=self.settings["name"])
@@ -0,0 +1,15 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"name": None,
"ifc_class": "IfcStructuralLoadLinearForce",
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
return self.file.create_entity(self.settings["ifc_class"], Name=self.settings["name"])
@@ -0,0 +1,26 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"name": "Unnamed",
"predefined_type": "LOAD_CASE",
"action_type": "NOTDEFINED",
"action_source": "NOTDEFINED",
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
load_case = ifcopenshell.api.run(
"root.create_entity",
self.file,
ifc_class="IfcStructuralLoadCase",
predefined_type=self.settings["predefined_type"],
name=self.settings["name"],
)
load_case.ActionType = self.settings["action_type"]
load_case.ActionSource = self.settings["action_source"]
return load_case
@@ -0,0 +1,26 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"name": "Unnamed",
"predefined_type": "LOAD_GROUP",
"action_type": "NOTDEFINED",
"action_source": "NOTDEFINED",
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
load_group = ifcopenshell.api.run(
"root.create_entity",
self.file,
ifc_class="IfcStructuralLoadGroup",
predefined_type=self.settings["predefined_type"],
name=self.settings["name"],
)
load_group.ActionType = self.settings["action_type"]
load_group.ActionSource = self.settings["action_source"]
return load_group
@@ -5,6 +5,14 @@ class Data:
connections = {}
boundary_conditions = {}
connects_structural_members = {}
members = {}
structural_activities = {}
structural_loads = {}
connects_structural_activities = {}
load_cases = {}
load_case_combinations = {}
load_groups = {}
@classmethod
def purge(cls):
@@ -14,6 +22,14 @@ class Data:
cls.connections = {}
cls.boundary_conditions = {}
cls.connects_structural_members = {}
cls.members = {}
cls.structural_activities = {}
cls.structural_loads = {}
cls.connects_structural_activities = {}
cls.load_cases = {}
cls.load_case_combinations = {}
cls.load_groups = {}
@classmethod
def load(cls, file, product_id=None):
@@ -21,8 +37,20 @@ class Data:
if not cls._file:
return
if product_id:
return cls.load_structural_connection(product_id)
product = cls._file.by_id(product_id)
if product.is_a("IfcStructuralConnection"):
return cls.load_structural_connection(product_id)
if product.is_a("IfcStructuralMember"):
return cls.load_structural_member(product_id)
# if product.is_a("IfcStructuralAction"):
# return cls.load_structural_action(product_id)
cls.load_structural_analysis_models()
cls.load_structural_load_cases()
cls.load_structural_load_case_combinations()
cls.load_structural_load_groups()
cls.load_structural_activities()
cls.load_structural_loads()
cls.load_boundary_conditions()
cls.is_loaded = True
@classmethod
@@ -37,7 +65,6 @@ class Data:
cls.products.setdefault(product.id(), []).append(model.id())
data = model.get_info()
del data["OwnerHistory"]
del data["OrientationOf2DPlane"]
loaded_by = []
for load_group in model.LoadedBy or []:
@@ -49,28 +76,98 @@ class Data:
has_results.append(result_group.id())
data["HasResults"] = has_results
data["OrientationOf2DPlane"] = model.OrientationOf2DPlane.id() if model.OrientationOf2DPlane else None
data["SharedPlacement"] = model.SharedPlacement.id() if model.SharedPlacement else None
cls.structural_analysis_models[model.id()] = data
@classmethod
def load_structural_load_cases(cls):
cls.load_cases = {}
for case in cls._file.by_type("IfcStructuralLoadCase"):
data = case.get_info()
del data["OwnerHistory"]
is_grouped_by = []
for rel in case.IsGroupedBy or []:
is_grouped_by.extend([o.id() for o in rel.RelatedObjects])
data["IsGroupedBy"] = is_grouped_by
cls.load_cases[case.id()] = data
@classmethod
def load_structural_load_case_combinations(cls):
cls.load_case_combinations = {}
for case in cls._file.by_type("IfcStructuralLoadGroup", include_subtypes=False):
if case.PredefinedType != "LOAD_COMBINATION":
continue
data = case.get_info()
del data["OwnerHistory"]
is_grouped_by = []
for load_group in case.IsGroupedBy or []:
is_grouped_by.append(load_group.id())
data["IsGroupedBy"] = is_grouped_by
cls.load_case_combinations[case.id()] = data
@classmethod
def load_structural_load_groups(cls):
cls.load_groups = {}
for case in cls._file.by_type("IfcStructuralLoadGroup", include_subtypes=False):
if case.PredefinedType == "LOAD_COMBINATION":
continue
data = case.get_info()
del data["OwnerHistory"]
is_grouped_by = []
for rel in case.IsGroupedBy or []:
is_grouped_by.extend([o.id() for o in rel.RelatedObjects])
data["IsGroupedBy"] = is_grouped_by
cls.load_groups[case.id()] = data
@classmethod
def load_structural_activities(cls):
cls.structural_activities = {}
for activity in cls._file.by_type("IfcStructuralActivity"):
data = activity.get_info()
del data["OwnerHistory"]
del data["ObjectPlacement"]
del data["Representation"]
data["AppliedLoad"] = data["AppliedLoad"].id() if data["AppliedLoad"] else None
data["AssignedToStructuralItem"] = None
if activity.AssignedToStructuralItem:
data["AssignedToStructuralItem"] = activity.AssignedToStructuralItem[0].RelatingElement.id()
cls.structural_activities[activity.id()] = data
@classmethod
def load_structural_connection(cls, product_id):
cls.connections = {}
cls.boundary_conditions = {}
cls.connects_structural_members = {}
connection = cls._file.by_id(product_id)
connection_data = {"AppliedCondition": None, "ConnectsStructuralMembers": []}
data = connection.get_info()
del data["OwnerHistory"]
data["ObjectPlacement"] = data["ObjectPlacement"].id() if data["ObjectPlacement"] is not None else None
data["Representation"] = data["Representation"].id() if data["Representation"] is not None else None
if connection.is_a("IfcStructuralCurveConnection"):
data["Axis"] = data["Axis"].id() if data["Axis"] is not None else None
if connection.is_a("IfcStructuralPointConnection"):
data["ConditionCoordinateSystem"] = data["ConditionCoordinateSystem"].id() if data["ConditionCoordinateSystem"] is not None else None
data["ConnectsStructuralMembers"] = []
if connection.AppliedCondition:
cls.load_boundary_condition(connection.AppliedCondition)
connection_data["AppliedCondition"] = connection.AppliedCondition.id()
data["AppliedCondition"] = connection.AppliedCondition.id()
for rel in connection.ConnectsStructuralMembers or []:
cls.load_connects_structural_member(rel)
connection_data["ConnectsStructuralMembers"].append(rel.id())
data["ConnectsStructuralMembers"].append(rel.id())
cls.connections[connection.id()] = connection_data
cls.connections[connection.id()] = data
@classmethod
def load_boundary_condition(cls, boundary_condition):
@@ -90,10 +187,68 @@ class Data:
del rel_data["ConditionCoordinateSystem"] # TODO: consider orientation
if rel.is_a("IfcRelConnectsWithEccentricity"):
rel_data["ConnectionConstraint"] = rel.ConnectionConstraint # TODO
rel_data["ConnectionConstraint"] = rel.ConnectionConstraint.id() # TODO
if rel.AppliedCondition:
cls.load_boundary_condition(rel.AppliedCondition)
rel_data["AppliedCondition"] = rel.AppliedCondition.id()
cls.connects_structural_members[rel.id()] = rel_data
@classmethod
def load_structural_loads(cls):
cls.structural_loads = {}
for load in cls._file.by_type("IfcStructuralLoad"):
cls.load_structural_load(load)
@classmethod
def load_structural_load(cls, load):
cls.structural_loads[load.id()] = load.get_info()
@classmethod
def load_boundary_conditions(cls):
cls.boundary_conditions = {}
for bc in cls._file.by_type("IfcBoundaryCondition"):
cls.load_boundary_condition(bc)
@classmethod
def load_connects_structural_activity(cls, rel):
rel_data = rel.get_info()
del rel_data["OwnerHistory"]
rel_data["RelatingElement"] = rel.RelatingElement.id()
rel_data["RelatedStructuralActivity"] = rel.RelatedStructuralActivity.id()
if rel.RelatedStructuralActivity.AppliedLoad:
cls.load_structural_load(rel.RelatedStructuralActivity.AppliedLoad)
# rel_data["AppliedCondition"] = rel.RelatedStructuralActivity.AppliedLoad.id()
cls.connects_structural_activities[rel.id()] = rel_data
@classmethod
def load_structural_member(cls, product_id):
cls.connects_structural_activities = {}
cls.connects_structural_members = {}
member = cls._file.by_id(product_id)
data = member.get_info()
del data["OwnerHistory"]
data["ObjectPlacement"] = data["ObjectPlacement"].id() if data["ObjectPlacement"] is not None else None
data["Representation"] = data["Representation"].id() if data["Representation"] is not None else None
if member.is_a("IfcStructuralCurveMember"):
data["Axis"] = data["Axis"].id() if data["Axis"] is not None else None
data["ConnectsStructuralActivities"] = []
data["ConnectedBy"] = []
for activity in member.AssignedStructuralActivity or []:
cls.load_connects_structural_activity(activity)
data["ConnectsStructuralActivities"].append(activity.id())
for rel in member.ConnectedBy or []:
cls.load_connects_structural_member(rel)
data["ConnectedBy"].append(rel.id())
cls.members[member.id()] = data
@@ -0,0 +1,23 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"structural_item": None, "axis": [0.0, 0.0, 1.0], "ref_direction": [1.0, 0.0, 0.0]}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.settings["structural_item"].ConditionCoordinateSystem is None:
point = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
ccs = self.file.createIfcAxis2Placement3D(point, None, None)
self.settings["structural_item"].ConditionCoordinateSystem = ccs
print(ccs)
ccs = self.settings["structural_item"].ConditionCoordinateSystem
print("use case")
print(ccs)
if ccs.Axis and len(self.file.get_inverse(ccs.Axis)) == 1:
self.file.remove(ccs.Axis)
ccs.Axis = self.file.createIfcDirection(self.settings["axis"])
if ccs.RefDirection and len(self.file.get_inverse(ccs.RefDirection)) == 1:
self.file.remove(ccs.RefDirection)
ccs.RefDirection = self.file.createIfcDirection(self.settings["ref_direction"])
@@ -0,0 +1,11 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"structural_item": None, "axis": [0.0, 0.0, 1.0]}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if len(self.file.get_inverse(self.settings["structural_item"].Axis)) == 1:
self.file.remove(self.settings["structural_item"].Axis)
self.settings["structural_item"].Axis = self.file.createIfcDirection(self.settings["axis"])
@@ -0,0 +1,13 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"structural_load": None,
"attributes": {}
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["structural_load"], name, value)
@@ -0,0 +1,10 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"load_case": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["load_case"], name, value)
@@ -1,11 +0,0 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"structural_member": None, "axis": [0.0, 0.0, 1.0]}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if self.file.get_inverse(self.settings["structural_member"].Axis) == 1:
self.file.remove(self.settings["structural_member"].Axis)
self.settings["structural_member"].Axis = self.file.createIfcDirection(self.settings["axis"])
@@ -1,13 +1,20 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"connection": None}
self.settings = {"connection": None, "boundary_condition": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
if not self.settings["connection"].AppliedCondition:
return
if len(self.file.get_inverse(self.settings["connection"].AppliedCondition)) == 1:
self.file.remove(self.settings["connection"].AppliedCondition)
self.settings["connection"].AppliedCondition = None
if self.settings["connection"]:
# remove boundary condition from a connection
if not self.settings["connection"].AppliedCondition:
return
if len(self.file.get_inverse(self.settings["connection"].AppliedCondition)) == 1:
self.file.remove(self.settings["connection"].AppliedCondition)
self.settings["connection"].AppliedCondition = None
else:
# remove the boundary condition
for conn in self.file.get_inverse(self.settings["boundary_condition"]):
conn.AppliedCondition = None
self.file.remove(self.settings["boundary_condition"])
@@ -0,0 +1,9 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"structural_load": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
self.file.remove(self.settings["structural_load"])
@@ -0,0 +1,15 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"load_case": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
# TODO: do a deep purge
for rel in self.settings["load_case"].IsGroupedBy or []:
self.file.remove(rel)
self.file.remove(self.settings["load_case"])
@@ -0,0 +1,16 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"load_group": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
# TODO: do a deep purge
for inverse in self.file.get_inverse(self.settings["load_group"]):
if inverse.is_a("IfcRelAssignsToGroup") and len(inverse.RelatedObjects) == 1:
self.file.remove(inverse)
self.file.remove(self.settings["load_group"])
@@ -2,15 +2,11 @@ class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"Name": "Name",
"SurfaceColour": [], # RGB
"DiffuseColour": [], # RGB
"Transparency": 0,
"external_definition": {
"Location": None,
"Identification": None,
"Name": "Name"
},
"name": "Name",
"surface_colour": [], # RGB
"diffuse_colour": [], # RGB
"transparency": 0,
"external_definition": {"location": None, "identification": None, "name": "Name"},
}
for key, value in settings.items():
self.settings[key] = value
@@ -20,22 +16,26 @@ class Usecase:
if self.settings["external_definition"]:
styles.append(self.create_externally_defined_surface_style())
# Name is filled out because Revit treats this incorrectly as the material name
return self.file.createIfcSurfaceStyle(self.settings["Name"], "BOTH", styles)
return self.file.createIfcSurfaceStyle(self.settings["name"], "BOTH", styles)
def create_surface_style_rendering(self):
return self.file.create_entity("IfcSurfaceStyleRendering", **{
"SurfaceColour": self.create_colour_rgb(self.settings["SurfaceColour"]),
"Transparency": (self.settings["Transparency"] - 1) * -1,
"ReflectanceMethod": "NOTDEFINED",
"DiffuseColour": self.create_colour_rgb(self.settings["DiffuseColour"])
})
return self.file.create_entity(
"IfcSurfaceStyleRendering",
**{
"SurfaceColour": self.create_colour_rgb(self.settings["surface_colour"]),
"Transparency": self.settings["transparency"],
"ReflectanceMethod": "NOTDEFINED",
"DiffuseColour": self.create_colour_rgb(self.settings["diffuse_colour"]),
}
)
def create_externally_defined_surface_style(self):
self.file.create_entity(
"IfcExternallyDefinedSurfaceStyle", **{
"Location": self.settings["Location"],
"Identification": self.settings["Identification"],
"Name": self.settings["Name"],
"IfcExternallyDefinedSurfaceStyle",
**{
"Location": self.settings["location"],
"Identification": self.settings["identification"],
"Name": self.settings["name"],
}
)
@@ -3,13 +3,13 @@ class Usecase:
self.file = file
self.settings = {
"style": None,
"SurfaceColour": [], # RGB
"DiffuseColour": [], # RGB
"Transparency": 0,
"surface_colour": [], # RGB
"diffuse_colour": [], # RGB
"transparency": 0,
"external_definition": {
"Location": None,
"Identification": None,
"Name": "Name"
"location": None,
"identification": None,
"name": "Name"
},
}
for key, value in settings.items():
@@ -20,20 +20,20 @@ class Usecase:
for element in self.file.traverse(self.settings["style"]):
if element.is_a("IfcSurfaceStyleShading"):
if element.SurfaceColour:
self.update_colour_rgb(element.SurfaceColour, self.settings["SurfaceColour"])
self.update_colour_rgb(element.SurfaceColour, self.settings["surface_colour"])
else:
element.SurfaceColour = self.create_colour_rgb(self.settings["SurfaceColour"])
element.Transparency = (self.settings["Transparency"] - 1) * -1
element.SurfaceColour = self.create_colour_rgb(self.settings["surface_colour"])
element.Transparency = (self.settings["transparency"] - 1) * -1
if element.is_a("IfcSurfaceStyleRendering"):
if element.DiffuseColour:
self.update_colour_rgb(element.DiffuseColour, self.settings["DiffuseColour"])
self.update_colour_rgb(element.DiffuseColour, self.settings["diffuse_colour"])
else:
element.DiffuseColour = self.create_colour_rgb(self.settings["DiffuseColour"])
element.DiffuseColour = self.create_colour_rgb(self.settings["diffuse_colour"])
# TODO: Move to separate usecase
#if element.is_a("IfcExternallyDefinedSurfaceStyle"):
# element.Location = self.settings["Location"]
# element.Identification = self.settings["Identification"]
# element.Name = self.settings["Name"]
# element.Location = self.settings["location"]
# element.Identification = self.settings["identification"]
# element.Name = self.settings["name"]
# has_external_definition = True
#if not has_external_definition:
# styles = list(self.settings["style"].Styles)
@@ -43,18 +43,18 @@ class Usecase:
def create_surface_style_rendering(self):
return self.file.create_entity("IfcSurfaceStyleRendering", **{
"SurfaceColour": self.create_colour_rgb(self.settings["SurfaceColour"]),
"Transparency": (self.settings["Transparency"] - 1) * -1,
"SurfaceColour": self.create_colour_rgb(self.settings["surface_colour"]),
"Transparency": (self.settings["transparency"] - 1) * -1,
"ReflectanceMethod": "NOTDEFINED",
"DiffuseColour": self.create_colour_rgb(self.settings["DiffuseColour"])
"DiffuseColour": self.create_colour_rgb(self.settings["diffuse_colour"])
})
def create_externally_defined_surface_style(self):
self.file.create_entity(
"IfcExternallyDefinedSurfaceStyle", **{
"Location": self.settings["Location"],
"Identification": self.settings["Identification"],
"Name": self.settings["Name"],
"Location": self.settings["location"],
"Identification": self.settings["identification"],
"Name": self.settings["name"],
}
)
@@ -1,5 +1,6 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
@@ -52,3 +53,50 @@ class Usecase:
"RelatingType": self.settings["relating_type"],
}
)
self.map_representations()
self.map_material_usages()
def map_representations(self):
if not self.settings["relating_type"].RepresentationMaps:
return
representations = []
if self.settings["related_object"].Representation:
representations = self.settings["related_object"].Representation.Representations
for representation in representations:
# TODO: check if this is right? Surely this can be a single usecase?
ifcopenshell.api.run(
"geometry.unassign_representation",
self.file,
**{"product": self.settings["related_object"], "representation": representation}
)
ifcopenshell.api.run("geometry.remove_representation", self.file, **{"representation": representation})
for representation_map in self.settings["relating_type"].RepresentationMaps:
representation = representation_map.MappedRepresentation
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", self.file, **{"representation": representation}
)
ifcopenshell.api.run(
"geometry.assign_representation",
self.file,
**{"product": self.settings["related_object"], "representation": mapped_representation}
)
def map_material_usages(self):
type_material = ifcopenshell.util.element.get_material(self.settings["relating_type"])
if not type_material:
return
if type_material.is_a("IfcMaterialLayerSet"):
ifcopenshell.api.run(
"material.assign_material",
self.file,
product=self.settings["related_object"],
type="IfcMaterialLayerSetUsage",
)
elif type_material.is_a("IfcMaterialProfileSet"):
ifcopenshell.api.run(
"material.assign_material",
self.file,
product=self.settings["related_object"],
type="IfcMaterialProfileSetUsage",
)
@@ -1,3 +1,6 @@
import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
@@ -15,6 +18,6 @@ class Usecase:
if rel.RelatingOpeningElement == self.settings["opening"]:
to_remove.append(rel)
break
self.file.remove(self.settings["opening"])
ifcopenshell.api.run("root.remove_product", self.file, product=self.settings["opening"])
for element in to_remove:
self.file.remove(element)
@@ -149,9 +149,14 @@ class tree(ifcopenshell_wrapper.tree):
args = [self, unwrap(value)]
if isinstance(value, entity_instance):
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs["extend"])
elif has_occ:
if isinstance(value, TopoDS.TopoDS_Shape):
args[1] = utils.serialize_shape(value)
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs["extend"])
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select(*args)]
def select_box(self, value, **kwargs):
+17 -2
View File
@@ -94,7 +94,7 @@ class entity(facet):
inst.is_a(self.name),
self.message % {"name": inst.is_a()}
)
class classification(facet):
"""
@@ -241,7 +241,7 @@ class restriction:
self.restriction_on = node['@base'][3:]
self.type = ""
self.options = []
for n in node:
if n[0:3] == "xs:":
if n[3:] == "enumeration":
@@ -274,6 +274,21 @@ class restriction:
#TODO add whiteSpace
else:
logger.error({'result':'ERROR', 'sentence':'Restriction not implemented'})
self.type = []
for n in node.childNodes:
if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"):
self.options.append(n.getAttribute("value"))
self.type = "enumeration"
elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")):
self.options.append(n.getAttribute("value"))
self.type = "bounds"
elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"):
self.options.append(n.getAttribute("value"))
self.type = "length"
elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"):
self.options.append(n.getAttribute("value"))
self.type = "pattern"
def __eq__(self, other):
result=False
@@ -0,0 +1,188 @@
###############################################################################
# #
# This file is part of IfcOpenShell. #
# #
# IfcOpenShell is free software: you can redistribute it and/or modify #
# it under the terms of the Lesser GNU General Public License as published by #
# the Free Software Foundation, either version 3.0 of the License, or #
# (at your option) any later version. #
# #
# IfcOpenShell is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# Lesser GNU General Public License for more details. #
# #
# You should have received a copy of the Lesser GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
###############################################################################
from enum import Enum
from dataclasses import dataclass
import math
from OCC.Core.gp import gp_Pnt2d
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeEdge2d
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeWire
class IfcTransitionCurveType(Enum):
"""IFC 4.1 Section 8.9.2.9
[https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/schema/ifcgeometryresource/lexical/ifctransitioncurvetype.htm]
The IfcTransitionCurveType indicates the curvature of a transition curve.
"""
BIQUADRATICPARABOLA = 1 # NOTE also referred to as Schramm curve.
BLOSSCURVE = 2
CLOTHOIDCURVE = 3
COSINECURVE = 4
CUBICPARABOLA = 5
SINECURVE = 6 # NOTE also referred to as Klein curve
@dataclass
class TransitionCurve:
"""
A curve that transitions between a straight line and a circular arc
(or the reverse).
"""
StartPoint: tuple # IfcSchema::IfcCartesianPoint
StartDirection: float # IfcSchema::IfcPlaneAngleMeasure
SegmentLength: float # IfcSchema::IfcPositiveLengthMeasure
IsStartRadiusCCW: bool # IfcSchema::IfcBoolean
IsEndRadiusCCW: bool # IfcSchema::IfcBoolean
TransitionCurveType: IfcTransitionCurveType
StartRadius: float = None # IfcSchema::IfcPositiveLengthMeasure
EndRadius: float = None # IfcSchema::IfcPositiveLengthMeasure
def _calc_biquadratic_parabola_point(self, lpt, L, R, ccw):
x = lpt
if (x <= (L / 2)):
y = x**4 / (6 * R * L**2)
else:
yterm_1 = (-1 * x**4) / (6 * R * L**2)
yterm_2 = (2 * x**3) / (3 * R * L)
yterm_3 = x**2 / (2 * R)
yterm_4 = (L * x) / (6 * R)
yterm_5 = L**2 / (48 * R)
y = yterm_1 + yterm_2 - yterm_3 + yterm_4 - yterm_5
if not ccw:
y = -y
return gp_Pnt2d(x, y)
def _calc_bloss_curve_point(self, lpt, L, R, ccw):
pass
def _calc_clothoid_curve_point(self, lpt, L, R, ccw):
RL = R * L
xterm_1 = 1
xterm_2 = lpt**4 / (40 * RL**2)
xterm_3 = lpt**8 / (3456 * RL**4)
xterm_4 = lpt**12 / (599040 * RL**6)
x = lpt * (xterm_1 - xterm_2 + xterm_3 - xterm_4)
factor = lpt**3 / (6 * RL)
yterm_1 = 1
yterm_2 = lpt**4 / (56 * RL**2)
yterm_3 = lpt**8 / (7040 * RL**4)
yterm_4 = lpt**12 / (1612800 * RL**6)
y = factor * (yterm_1 - yterm_2 + yterm_3 - yterm_4)
if not ccw:
y = -y
return gp_Pnt2d(x, y)
def _calc_cosine_curve_point(self, lpt, L, R, ccw):
pi = math.pi
psi_x = (pi * lpt) / L
xterm_1 = (L**2) / (8.0 * pi**2 * R**2)
xterm_2 = L / pi
xterm_3 = psi_x**3 / (3.0)
xterm_4 = psi_x / (2.0)
xterm_5 = (math.sin(psi_x) * math.cos(psi_x)) / (2.0)
xterm_6 = psi_x * math.cos(psi_x)
x = lpt - xterm_1 * xterm_2 * ( xterm_3 + xterm_4 - xterm_5 - (2.0 * xterm_6))
# TODO: code for y - coordinate
y = 0
if not ccw:
y = -y
return gp_Pnt2d(x, y)
def _calc_cubic_parabola_point(self, lpt, L, R, ccw):
x = lpt
y = math.pow(x, 3) / (6 * R * L)
if not ccw:
y = -y
return gp_Pnt2d(x, y)
def _calc_sine_curve_point(self, lpt, L, R, ccw):
pass
def _calc_transition_curve_point(self, lpt, L, R, ccw, trans_type):
if trans_type == "BIQUADRATICPARABOLA":
return self._calc_cubic_parabola_point(lpt, L, R, ccw)
elif trans_type == "BLOSSCURVE":
# return _calc_bloss_curve_point(lpt, L, R, ccw)
raise ValueError(f"Transition Curve type '{trans_type}' not implemented yet.")
elif trans_type == "CLOTHOIDCURVE":
return self._calc_clothoid_curve_point(lpt, L, R, ccw)
elif trans_type == "COSINECURVE":
# return _calc_cosine_curve_point(lpt, L, R, ccw)
raise ValueError(f"Transition Curve type '{trans_type}' not implemented yet.")
elif trans_type == "CUBICPARABOLA":
return self._calc_cubic_parabola_point(lpt, L, R, ccw)
elif trans_type == "SINECURVE":
# return _calc_sine_curve_point(lpt, L, R, ccw)
raise ValueError(f"Transition Curve type '{trans_type}' not implemented yet.")
else:
raise ValueError(f"Invalid Transition Curve type '{trans_type}'.")
def to_wire(self, stroking_interval=5.0):
"""convert IfcTransitionSegment2D to OCC wire
:param stroking_interval: maximum curve length between points to be calculated
:type stroking_interval: float
:return: OCC wire containing interpolated points
"""
points = list()
L = self.SegmentLength
R = self.EndRadius
ccw = self.IsStartRadiusCCW
trans_type = self.TransitionCurveType.name
num_intervals = math.ceil(L / stroking_interval)
interval_dist = L / num_intervals
lpt = 0.0 # length along the curve at the point to be calculated
for _ in range(num_intervals):
points.append(self._calc_transition_curve_point(
lpt, L, R, ccw, trans_type
))
lpt += interval_dist
edges = list()
for i in range(len(points) - 1):
edges.append(BRepBuilderAPI_MakeEdge2d(
points[i], points[i + 1]
))
wire = BRepBuilderAPI_MakeWire()
for e in edges:
wire.Add(e.Edge())
# return wire
return points
@@ -1,18 +1,33 @@
import datetime
from re import findall
try:
import isodate
except:
pass # Duration parsing not supported
def duration2dict(duration):
results = {}
for number, unit in findall("(?P<number>\d+)(?P<period>S|M|H|D|W|Y)", duration):
results[unit] = number
return results
def timedelta2duration(timedelta):
components = {
"days": getattr(timedelta, "days", 0),
"hours": 0,
"minutes": 0,
"seconds": getattr(timedelta, "seconds", 0),
}
if components["seconds"]:
components["hours"], components["minutes"], components["seconds"] = [
int(i) for i in str(datetime.timedelta(seconds=components["seconds"])).split(":")
]
return isodate.Duration(**components)
def ifc2datetime(element):
if isinstance(element, str) and element[0] == "P": # IfcDuration
return duration2dict(element)
elif isinstance(element, str) and element[2] == ":": # IfcTime
if isinstance(element, str) and "P" in element[0:2]: # IfcDuration
duration = isodate.parse_duration(element)
if isinstance(duration, datetime.timedelta):
return timedelta2duration(duration)
return duration
elif isinstance(element, str) and len(element) > 3 and element[2] == ":": # IfcTime
return datetime.time.fromisoformat(element)
elif isinstance(element, str) and ":" in element: # IfcDateTime
return datetime.datetime.fromisoformat(element)
@@ -40,8 +55,13 @@ def ifc2datetime(element):
def datetime2ifc(dt, ifc_type):
if isinstance(dt, str):
if ifc_type == "IfcDuration":
return dt
dt = datetime.datetime.fromisoformat(dt)
if ifc_type == "IfcTimeStamp":
if ifc_type == "IfcDuration":
return isodate.duration_isoformat(dt)
elif ifc_type == "IfcTimeStamp":
return int(dt.timestamp())
elif ifc_type == "IfcDateTime":
if isinstance(dt, datetime.datetime):
@@ -1,20 +1,17 @@
import ifcopenshell
def get_psets(element):
psets = {}
try:
if element.is_a("IfcTypeObject"):
if element.HasPropertySets:
for definition in element.HasPropertySets:
psets[definition.Name] = get_property_definition(definition)
else:
for relationship in element.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
definition = relationship.RelatingPropertyDefinition
psets[definition.Name] = get_property_definition(definition)
except Exception as e:
import traceback
print("failed to load properties: {}".format(e))
traceback.print_exc()
if element.is_a("IfcTypeObject"):
if element.HasPropertySets:
for definition in element.HasPropertySets:
psets[definition.Name] = get_property_definition(definition)
elif hasattr(element, "IsDefinedBy"):
for relationship in element.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
definition = relationship.RelatingPropertyDefinition
psets[definition.Name] = get_property_definition(definition)
return psets
@@ -55,7 +52,9 @@ def get_properties(properties):
def get_type(element):
if hasattr(element, "IsTypedBy") and element.IsTypedBy:
if element.is_a("IfcTypeObject"):
return element
elif hasattr(element, "IsTypedBy") and element.IsTypedBy:
return element.IsTypedBy[0].RelatingType
elif hasattr(element, "IsDefinedBy") and element.IsDefinedBy: # IFC2X3
for relationship in element.IsDefinedBy:
@@ -63,23 +62,20 @@ def get_type(element):
return relationship.RelatingType
def get_material(element):
def get_material(element, should_skip_usage=False):
if hasattr(element, "HasAssociations") and element.HasAssociations:
for relationship in element.HasAssociations:
if relationship.is_a("IfcRelAssociatesMaterial"):
if relationship.RelatingMaterial.is_a("IfcMaterialLayerSetUsage"):
return relationship.RelatingMaterial.ForLayerSet
elif relationship.RelatingMaterial.is_a("IfcMaterialProfileSetUsage"):
return relationship.RelatingMaterial.ForProfileSet
if should_skip_usage:
if relationship.RelatingMaterial.is_a("IfcMaterialLayerSetUsage"):
return relationship.RelatingMaterial.ForLayerSet
elif relationship.RelatingMaterial.is_a("IfcMaterialProfileSetUsage"):
return relationship.RelatingMaterial.ForProfileSet
return relationship.RelatingMaterial
relating_type = get_type(element)
if hasattr(relating_type, "HasAssociations") and relating_type.HasAssociations:
for relationship in relating_type.HasAssociations:
if relationship.is_a("IfcRelAssociatesMaterial"):
if relationship.RelatingMaterial.is_a("IfcMaterialLayerSetUsage"):
return relationship.RelatingMaterial.ForLayerSet
elif relationship.RelatingMaterial.is_a("IfcMaterialProfileSetUsage"):
return relationship.RelatingMaterial.ForProfileSet
return relationship.RelatingMaterial
@@ -105,24 +101,6 @@ def replace_attribute(element, old, new):
element[i] = new_attribute
def is_representation_of_context(representation, context, subcontext=None, target_view=None):
if target_view is not None:
return (
representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext")
and representation.ContextOfItems.TargetView == target_view
and representation.ContextOfItems.ContextIdentifier == subcontext
and representation.ContextOfItems.ContextType == context
)
elif subcontext is not None:
return (
representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext")
and representation.ContextOfItems.ContextIdentifier == subcontext
and representation.ContextOfItems.ContextType == context
)
elif representation.ContextOfItems.ContextType == context:
return True
def remove_deep(ifc_file, element):
# @todo maybe some sort of try-finally mechanism.
ifc_file.batch()
@@ -134,12 +112,25 @@ def remove_deep(ifc_file, element):
ifc_file.unbatch()
def get_representation(element, context, subcontext=None, target_view=None):
if element.is_a("IfcProduct") and element.Representation:
for r in element.Representation.Representations:
if is_representation_of_context(r, context, subcontext, target_view):
return r
elif element.is_a("IfcTypeProduct") and element.RepresentationMaps:
for r in element.RepresentationMaps:
if is_representation_of_context(r.MappedRepresentation, context, subcontext, target_view):
return r.MappedRepresentation
def copy(ifc_file, element):
new = ifc_file.create_entity(element.is_a())
for i, attribute in enumerate(element):
if attribute is None:
continue
new[i] = attribute
return new
def copy_deep(ifc_file, element):
new = ifc_file.create_entity(element.is_a())
for i, attribute in enumerate(element):
if attribute is None:
continue
if isinstance(attribute, ifcopenshell.entity_instance):
attribute = copy_deep(ifc_file, attribute)
elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance):
attribute = list(attribute)
for j, item in enumerate(attribute):
attribute[j] = copy_deep(ifc_file, item)
new[i] = attribute
return new
@@ -0,0 +1,42 @@
def get_context(ifc_file, context, subcontext=None, target_view=None):
if subcontext or target_view:
elements = ifc_file.by_type("IfcGeometricRepresentationSubContext")
else:
elements = ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
for element in elements:
if context and element.ContextType != context:
continue
if subcontext and getattr(element, "ContextIdentifier") != subcontext:
continue
if target_view and getattr(element, "TargetView") != target_view:
continue
return element
def is_representation_of_context(representation, context, subcontext=None, target_view=None):
if target_view is not None:
return (
representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext")
and representation.ContextOfItems.TargetView == target_view
and representation.ContextOfItems.ContextIdentifier == subcontext
and representation.ContextOfItems.ContextType == context
)
elif subcontext is not None:
return (
representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext")
and representation.ContextOfItems.ContextIdentifier == subcontext
and representation.ContextOfItems.ContextType == context
)
elif representation.ContextOfItems.ContextType == context:
return True
def get_representation(element, context, subcontext=None, target_view=None):
if element.is_a("IfcProduct") and element.Representation:
for r in element.Representation.Representations:
if is_representation_of_context(r, context, subcontext, target_view):
return r
elif element.is_a("IfcTypeProduct") and element.RepresentationMaps:
for r in element.RepresentationMaps:
if is_representation_of_context(r.MappedRepresentation, context, subcontext, target_view):
return r.MappedRepresentation
@@ -221,7 +221,7 @@ class Selector:
key = ".".join(key.split(".")[1:])
elif "." in key and key.split(".")[0] == "material":
try:
element = ifcopenshell.util.element.get_material(element)
element = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if not element:
return None
except:
@@ -0,0 +1,133 @@
import datetime
import ifcopenshell.util.date
from functools import lru_cache
def derive_calendar(task):
calendar = [
rel.RelatingControl
for rel in task.HasAssignments or []
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar")
]
if calendar:
return calendar[0]
for rel in task.Nests or []:
return derive_calendar(rel.RelatingObject)
def count_working_days(start, finish, calendar):
result = 0
current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day)
while current_date < finish_date:
if is_working_day(current_date, calendar):
result += 1
current_date += datetime.timedelta(days=1)
return result
def get_finish_date(start, duration, duration_type, calendar):
current_date = datetime.date(start.year, start.month, start.day)
abs_duration = abs(duration.days)
date_offset = datetime.timedelta(days=1 if duration.days > 0 else -1)
while abs_duration > 0:
if duration_type == "ELAPSEDTIME" or not calendar:
abs_duration -= 1
elif ifcopenshell.util.sequence.is_working_day(current_date, calendar):
abs_duration -= 1
current_date += date_offset
if duration.days > 0:
current_date = get_soonest_working_day(current_date, duration_type, calendar)
else:
current_date = get_recent_working_day(current_date, duration_type, calendar)
return current_date
def get_soonest_working_day(start, duration_type, calendar):
if duration_type == "ELAPSEDTIME" or not calendar:
return start
while not is_working_day(start, calendar):
start += datetime.timedelta(days=1)
return start
def get_recent_working_day(start, duration_type, calendar):
if duration_type == "ELAPSEDTIME" or not calendar:
return start
while not is_working_day(start, calendar):
start -= datetime.timedelta(days=1)
return start
@lru_cache(maxsize=None)
def is_working_day(day, calendar):
is_working_day = False
for work_time in calendar.WorkingTimes or []:
if is_work_time_applicable_to_day(work_time, day):
is_working_day = True
break
if not is_working_day:
return is_working_day
for work_time in calendar.ExceptionTimes or []:
if is_work_time_applicable_to_day(work_time, day):
is_working_day = False
break
return is_working_day
def is_work_time_applicable_to_day(work_time, day):
start = None
finish = None
if isinstance(day, datetime.datetime):
day = datetime.date(day.year, day.month, day.day)
if work_time.Start:
start = ifcopenshell.util.date.ifc2datetime(work_time.Start)
if start > day:
return False
if work_time.Finish:
finish = ifcopenshell.util.date.ifc2datetime(work_time.Finish)
if finish < day:
return False
if not work_time.RecurrencePattern:
return True
recurrence = work_time.RecurrencePattern
if recurrence.RecurrenceType == "DAILY":
if not recurrence.Interval and not recurrence.Occurrences:
return True
if not work_time.Start:
return False
return False # TODO
elif recurrence.RecurrenceType == "WEEKLY":
if not recurrence.Interval and not recurrence.Occurrences:
return (day.weekday() + 1) in recurrence.WeekdayComponent
if not work_time.Start:
return False
return False # TODO
elif recurrence.RecurrenceType == "MONTHLY_BY_DAY_OF_MONTH":
if not recurrence.Interval and not recurrence.Occurrences:
return day.day in recurrence.DayComponent
return False # TODO
elif recurrence.RecurrenceType == "MONTHLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
return (day.weekday() + 1) in recurrence.WeekdayComponent and math.floor(day.day / 7) + 1 == recurrence[
"Position"
]
return False # TODO
elif recurrence.RecurrenceType == "YEARLY_BY_DAY_OF_MONTH":
if not recurrence.Interval and not recurrence.Occurrences:
return day.month in recurrence.MonthComponent and day.day in recurrence.DayComponent
return False # TODO
elif recurrence.RecurrenceType == "YEARLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
return (
day.month in recurrence.MonthComponent
and (day.weekday() + 1) in recurrence.WeekdayComponent
and math.floor(day.day / 7) + 1 == recurrence.Position
)
return False # TODO