mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 04:32:23 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84c931bb5f | |||
| 572aedb3eb | |||
| 1db86c24e3 | |||
| 74adde3030 | |||
| f68e8f0e00 | |||
| 98386984f3 | |||
| 7b35ed97ca | |||
| b3302ed679 | |||
| e12f060af8 | |||
| 6d4f47a2ca | |||
| e49ab7e0cc | |||
| ccfe7ab80a | |||
| 7b95358975 | |||
| bcc50aaadc | |||
| 30f63b7d67 | |||
| ead13e23bc | |||
| 24257d2a85 | |||
| 5c4dbb924d | |||
| 88e1b3d2f9 | |||
| f67b80c6a6 | |||
| c7fd42f2cf | |||
| bed03bf83c | |||
| 1e49400e93 | |||
| 608c0dc05d | |||
| 921dbe9dae | |||
| a7e0837f96 | |||
| 071c80f845 | |||
| 06d823b9d8 | |||
| b15e33e5e9 |
@@ -137,7 +137,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9838698-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -908,7 +908,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
index += 1
|
||||
pset = tool.Ifc.get().by_id(pset['id'])
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
@@ -976,26 +976,23 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
add_linked_aggregate_pset(element, index)
|
||||
index +=1
|
||||
index += 1
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
select_objects_and_add_data(part)
|
||||
else:
|
||||
add_linked_aggregate_pset(part, index)
|
||||
index += 1
|
||||
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
|
||||
|
||||
def add_linked_aggregate_pset(part, index):
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
|
||||
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
@@ -1029,21 +1026,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
][0]
|
||||
|
||||
|
||||
number = len(group_elements) - 1
|
||||
number = f"{number:02d}"
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
pattern1 = r'_\d'
|
||||
pattern1 = r"_\d"
|
||||
if re.findall(pattern1, new_obj.name):
|
||||
split_name = new_obj.name.split("_")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
continue
|
||||
pattern2 = r'\.\d{3}'
|
||||
pattern2 = r"\.\d{3}"
|
||||
if re.findall(pattern2, new_obj.name):
|
||||
split_name = new_obj.name.split(".")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1065,7 +1061,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
|
||||
# Recreate aggregate relationship
|
||||
for old in old_to_new.keys():
|
||||
if old.is_a("IfcElementAssembly"):
|
||||
@@ -1075,17 +1071,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
return old_to_new
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i['Index'] for i in psets if i]
|
||||
index = [i["Index"] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
@@ -1134,9 +1128,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names[group] = {}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(element).name
|
||||
|
||||
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
for part in parts:
|
||||
@@ -1144,20 +1138,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names | get_original_names(part)
|
||||
else:
|
||||
try:
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
except:
|
||||
continue
|
||||
index = pset['Index']
|
||||
continue
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(part).name
|
||||
|
||||
|
||||
return original_names
|
||||
|
||||
def set_original_name(obj, original_names):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
if ifcopenshell.util.element.get_parts(
|
||||
element
|
||||
): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
aggregate = element
|
||||
|
||||
|
||||
group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
@@ -1166,18 +1162,16 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
]
|
||||
if not group:
|
||||
return
|
||||
|
||||
|
||||
group = group[0].id()
|
||||
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
|
||||
index = pset["Index"]
|
||||
|
||||
try:
|
||||
obj.name = original_names[group][index]
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
|
||||
def get_element_assembly(element):
|
||||
if element.is_a("IfcElementAssembly"):
|
||||
@@ -1263,9 +1257,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
selected_matrix = selected_obj.matrix_world
|
||||
object_duplicate = tool.Ifc.get_object(element)
|
||||
duplicate_matrix = object_duplicate.matrix_world.decompose()
|
||||
|
||||
|
||||
original_names = get_original_names(element)
|
||||
|
||||
|
||||
delete_objects(element)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
@@ -1280,24 +1274,24 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
|
||||
new_obj_matrix = new_base_matrix @ matrix_diff
|
||||
new_obj.matrix_world = new_obj_matrix
|
||||
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
if element_aggregate and new[0].is_a("IfcElementAssembly"):
|
||||
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
|
||||
|
||||
if not new_aggregate:
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
set_original_name(new_obj, original_names)
|
||||
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
operator_time = time() - refresh_start_time
|
||||
|
||||
@@ -118,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_material"
|
||||
bl_label = "Add Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Material Name")
|
||||
name: bpy.props.StringProperty(default="Default")
|
||||
|
||||
def invoke(self, context, event):
|
||||
|
||||
@@ -113,6 +113,10 @@ class CreateProject(bpy.types.Operator):
|
||||
if tool.Blender.is_default_scene():
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
for mesh in bpy.data.meshes:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
for mat in bpy.data.materials:
|
||||
bpy.data.materials.remove(mat)
|
||||
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
|
||||
tool.Blender.register_toolbar()
|
||||
|
||||
|
||||
@@ -25,8 +25,16 @@ from shapely.geometry import Polygon
|
||||
from shapely.ops import unary_union
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim
|
||||
from typing import Literal, Union, Optional
|
||||
|
||||
|
||||
AxisType = Literal["x", "y", "z"]
|
||||
VectorTuple = tuple[float, float, float]
|
||||
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
|
||||
|
||||
|
||||
class QtoCalculator:
|
||||
@@ -45,7 +53,7 @@ class QtoCalculator:
|
||||
else:
|
||||
self.mapping_dict[key][item] = None
|
||||
|
||||
def calculate_quantity(self, qto_name, quantity_name, obj):
|
||||
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
"""calculates the value of the quantity in the project units"""
|
||||
string = "self.mapping_dict[qto_name][quantity_name](obj"
|
||||
if isinstance(mapper[qto_name][quantity_name], dict):
|
||||
@@ -54,11 +62,13 @@ class QtoCalculator:
|
||||
args = ""
|
||||
string += args
|
||||
string += ")"
|
||||
value = eval(string)
|
||||
value: float = eval(string)
|
||||
|
||||
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
|
||||
|
||||
def guess_quantity(self, prop_name, alternative_prop_names, obj):
|
||||
def guess_quantity(
|
||||
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
|
||||
) -> Union[float, None]:
|
||||
"""guess the value of the quantity by name, returns the value in the project units"""
|
||||
prop_name = prop_name.lower()
|
||||
alternative_prop_names = [p.lower() for p in alternative_prop_names]
|
||||
@@ -89,7 +99,7 @@ class QtoCalculator:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
unit_type_keywords = {
|
||||
unit_type_keywords: dict[str, QuanityTypes] = {
|
||||
"length": "Q_LENGTH",
|
||||
"width": "Q_LENGTH",
|
||||
"height": "Q_LENGTH",
|
||||
@@ -102,10 +112,10 @@ class QtoCalculator:
|
||||
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
|
||||
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
|
||||
|
||||
def get_units(self, o, vg_index):
|
||||
def get_units(self, o: bpy.types.Object, vg_index: int) -> int:
|
||||
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
|
||||
|
||||
def get_linear_length(self, o):
|
||||
def get_linear_length(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the length of the longest edge of the object bounding box
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -116,7 +126,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x, y, z)
|
||||
|
||||
def get_length(self, o, vg_index=None, main_axis: str = ""):
|
||||
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
|
||||
if vg_index is None:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
@@ -141,23 +151,23 @@ class QtoCalculator:
|
||||
length += self.get_edge_distance(o, e)
|
||||
return length
|
||||
|
||||
def get_stair_length(self, obj):
|
||||
def get_stair_length(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj)
|
||||
height = self.get_height(obj)
|
||||
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
|
||||
return stair_length
|
||||
|
||||
def get_net_stair_area(self, obj):
|
||||
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
|
||||
return OBB_net_footprint_area
|
||||
|
||||
def get_gross_stair_area(self, obj):
|
||||
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
|
||||
return OBB_gross_footprint_area
|
||||
|
||||
def get_parametric_axis(self, obj):
|
||||
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
|
||||
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
|
||||
if relating_type:
|
||||
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
||||
@@ -172,7 +182,7 @@ class QtoCalculator:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_covering_gross_area(self, obj):
|
||||
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -181,7 +191,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_gross_footprint_area(obj)
|
||||
|
||||
def get_covering_net_area(self, obj):
|
||||
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_net_footprint_area(obj)
|
||||
@@ -190,7 +200,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_net_footprint_area(obj)
|
||||
|
||||
def get_covering_width(self, obj):
|
||||
def get_covering_width(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_height(obj)
|
||||
@@ -199,7 +209,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_width(self, o):
|
||||
def get_width(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the width of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -209,7 +219,7 @@ class QtoCalculator:
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
return min(x, y)
|
||||
|
||||
def get_height(self, o):
|
||||
def get_height(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the height of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -217,32 +227,32 @@ class QtoCalculator:
|
||||
"""
|
||||
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
def get_opening_height(self, obj):
|
||||
def get_opening_height(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_width(obj)
|
||||
else:
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_opening_depth(self, obj):
|
||||
def get_opening_depth(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_height(obj)
|
||||
else:
|
||||
return self.get_width(obj)
|
||||
|
||||
def get_opening_mapping_area(self, obj):
|
||||
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_net_footprint_area(obj)
|
||||
else:
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
def get_finish_ceiling_height(self, obj):
|
||||
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
space_height = self.get_height(obj)
|
||||
floor_height = self.get_finish_floor_height(obj)
|
||||
ceiling_height = self.get_ceiling_height(obj)
|
||||
finish_ceiling_height = space_height - floor_height - ceiling_height
|
||||
return finish_ceiling_height
|
||||
|
||||
def get_finish_floor_height(self, obj):
|
||||
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_floor_height = 0
|
||||
@@ -258,7 +268,7 @@ class QtoCalculator:
|
||||
|
||||
return finish_floor_height
|
||||
|
||||
def get_ceiling_height(self, obj):
|
||||
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_ceiling_height = 0
|
||||
@@ -274,7 +284,7 @@ class QtoCalculator:
|
||||
|
||||
return finish_ceiling_height
|
||||
|
||||
def get_net_perimeter(self, o):
|
||||
def get_net_perimeter(self, o: bpy.types.Object) -> float:
|
||||
parsed_edges = []
|
||||
shared_edges = []
|
||||
perimeter = 0
|
||||
@@ -289,7 +299,7 @@ class QtoCalculator:
|
||||
perimeter -= self.get_edge_key_distance(o, edge_key)
|
||||
return perimeter
|
||||
|
||||
def get_gross_perimeter(self, o):
|
||||
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(o)
|
||||
mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("GrossObj", mesh)
|
||||
@@ -297,15 +307,15 @@ class QtoCalculator:
|
||||
self.delete_obj(gross_obj)
|
||||
return gross_perimeter
|
||||
|
||||
def get_space_net_perimeter(self, obj):
|
||||
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
pass
|
||||
|
||||
def get_rectangular_perimeter(self, obj):
|
||||
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj, main_axis="x")
|
||||
height = self.get_height(obj)
|
||||
return (length + height) * 2
|
||||
|
||||
def get_lowest_polygons(self, o):
|
||||
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
lowest_polygons = []
|
||||
lowest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -321,7 +331,7 @@ class QtoCalculator:
|
||||
lowest_z = z
|
||||
return lowest_polygons
|
||||
|
||||
def get_highest_polygons(self, o):
|
||||
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
highest_polygons = []
|
||||
highest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -337,13 +347,13 @@ class QtoCalculator:
|
||||
highest_z = z
|
||||
return highest_polygons
|
||||
|
||||
def get_edge_key_distance(self, obj, edge_key):
|
||||
def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
|
||||
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
|
||||
|
||||
def get_edge_distance(self, obj, edge):
|
||||
def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
|
||||
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
|
||||
|
||||
def get_net_floor_area(self, obj):
|
||||
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -359,7 +369,7 @@ class QtoCalculator:
|
||||
|
||||
return total_net_floor_area
|
||||
|
||||
def get_gross_ceiling_area(self, obj):
|
||||
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_top_area(obj)
|
||||
@@ -375,7 +385,7 @@ class QtoCalculator:
|
||||
|
||||
return total_gross_ceiling_area
|
||||
|
||||
def get_net_ceiling_area(self, obj):
|
||||
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_net_top_area(obj)
|
||||
@@ -395,7 +405,7 @@ class QtoCalculator:
|
||||
|
||||
return total_net_ceiling_area
|
||||
|
||||
def get_space_net_volume(self, obj):
|
||||
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_volume(obj)
|
||||
@@ -410,7 +420,7 @@ class QtoCalculator:
|
||||
|
||||
return total_space_net_volume
|
||||
|
||||
def get_net_footprint_area(self, o):
|
||||
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -421,7 +431,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_gross_footprint_area(self, o):
|
||||
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -437,7 +447,7 @@ class QtoCalculator:
|
||||
self.delete_mesh(mesh)
|
||||
return gross_footprint_area
|
||||
|
||||
def get_net_roofprint_area(self, o):
|
||||
def get_net_roofprint_area(self, o: bpy.types.Object) -> float:
|
||||
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
|
||||
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
|
||||
|
||||
@@ -449,7 +459,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_side_area(self, o):
|
||||
def get_side_area(self, o: bpy.types.Object) -> float:
|
||||
# There are a few dumb options for this, but this seems the dumbest
|
||||
# until I get more practical experience on what works best.
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
@@ -457,8 +467,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x * z, y * z)
|
||||
|
||||
def get_cross_section_area(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
@@ -474,7 +483,7 @@ class QtoCalculator:
|
||||
return area
|
||||
# TODO handle other types of sections, and then fall back to mesh parsing
|
||||
|
||||
def get_gross_surface_area(self, o, vg_index=None):
|
||||
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
|
||||
if vg_index is None:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_surface_area(o)
|
||||
@@ -492,29 +501,29 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_net_surface_area(self, obj):
|
||||
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
return self.get_mesh_area(obj.data)
|
||||
|
||||
def get_mesh_area(self, mesh):
|
||||
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
|
||||
area = 0
|
||||
for polygon in mesh.polygons:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def is_polygon_in_vg(self, polygon, vertices_in_vg):
|
||||
def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
|
||||
for v in polygon.vertices:
|
||||
if v not in vertices_in_vg:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_net_volume(self, o):
|
||||
def get_net_volume(self, o: bpy.types.Object) -> float:
|
||||
o_mesh = bmesh.new()
|
||||
o_mesh.from_mesh(o.data)
|
||||
volume = o_mesh.calc_volume()
|
||||
o_mesh.free()
|
||||
return volume
|
||||
|
||||
def get_gross_volume(self, o):
|
||||
def get_gross_volume(self, o: bpy.types.Object) -> float:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_volume(o)
|
||||
|
||||
@@ -529,16 +538,18 @@ class QtoCalculator:
|
||||
|
||||
return gross_volume
|
||||
|
||||
def has_openings(self, obj):
|
||||
def has_openings(
|
||||
self, obj: bpy.types.Object
|
||||
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return element and getattr(element, "HasOpenings", [])
|
||||
|
||||
def get_obj_decompositions(self, obj):
|
||||
def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
return decompositions
|
||||
|
||||
def get_gross_weight(self, obj):
|
||||
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -546,7 +557,7 @@ class QtoCalculator:
|
||||
gross_weight = obj_mass_density * gross_volume
|
||||
return gross_weight
|
||||
|
||||
def get_net_weight(self, obj):
|
||||
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -554,7 +565,7 @@ class QtoCalculator:
|
||||
net_weight = obj_mass_density * net_volume
|
||||
return net_weight
|
||||
|
||||
def get_obj_mass_density(self, obj):
|
||||
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
material = ifcopenshell.util.element.get_material(entity)
|
||||
if material is None:
|
||||
@@ -629,7 +640,7 @@ class QtoCalculator:
|
||||
# volume += v1.dot(v2.cross(v3)) / 6.0
|
||||
# return volume
|
||||
|
||||
def get_opening_type(self, opening, obj):
|
||||
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
|
||||
"""_summary_: Returns the opening type - OPENING / RECESS
|
||||
|
||||
:param blender-object opening: blender opening object
|
||||
@@ -649,14 +660,22 @@ class QtoCalculator:
|
||||
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
|
||||
|
||||
def get_opening_area(
|
||||
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
|
||||
):
|
||||
self,
|
||||
obj: bpy.types.Object,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
min_area: int = 0,
|
||||
ignore_recesses: bool = False,
|
||||
) -> float:
|
||||
"""_summary_: Returns the lateral area of the openings in the object.
|
||||
|
||||
:param obj: blender object
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
|
||||
defaults to 0
|
||||
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
|
||||
:return float: Opening Area
|
||||
"""
|
||||
@@ -702,14 +721,14 @@ class QtoCalculator:
|
||||
|
||||
def get_lateral_area(
|
||||
self,
|
||||
obj,
|
||||
obj: bpy.types.Object,
|
||||
subtract_openings: bool = True,
|
||||
exclude_end_areas: bool = False,
|
||||
exclude_side_areas: bool = False,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
main_axis: str = "",
|
||||
):
|
||||
) -> float:
|
||||
"""_summary_
|
||||
|
||||
:param blender-object obj: blender object, bpy.types.Object
|
||||
@@ -760,7 +779,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area + total_opening_area
|
||||
|
||||
def get_gross_side_area(self, obj):
|
||||
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
|
||||
if not self.has_openings(obj):
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
@@ -768,15 +787,15 @@ class QtoCalculator:
|
||||
|
||||
return gross_side_area
|
||||
|
||||
def get_net_side_area(self, obj):
|
||||
def get_net_side_area(self, obj: bpy.types.Object) -> float:
|
||||
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
|
||||
return net_side_area
|
||||
|
||||
def get_outer_surface_area(self, obj):
|
||||
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
|
||||
return outer_surface_area
|
||||
|
||||
def get_end_area(self, obj):
|
||||
def get_end_area(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
gross_mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
|
||||
@@ -790,7 +809,7 @@ class QtoCalculator:
|
||||
|
||||
return end_area
|
||||
|
||||
def get_gross_top_area(self, obj, angle: int = 45):
|
||||
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
|
||||
"""_summary_: Returns the gross top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
@@ -827,12 +846,14 @@ class QtoCalculator:
|
||||
return area + opening_area
|
||||
|
||||
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
|
||||
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
|
||||
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
|
||||
"""_summary_: Returns the net top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
|
||||
Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
|
||||
defaults to True
|
||||
:return float: Net Top Area
|
||||
"""
|
||||
z_axis = (0, 0, 1)
|
||||
@@ -852,11 +873,11 @@ class QtoCalculator:
|
||||
|
||||
return area
|
||||
|
||||
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
|
||||
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
|
||||
"""_summary_: Returns the projected area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
|
||||
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
|
||||
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
|
||||
:return float: Projected Area
|
||||
"""
|
||||
@@ -891,7 +912,7 @@ class QtoCalculator:
|
||||
return projected_polygon.area + void_area
|
||||
return projected_polygon.area
|
||||
|
||||
def get_OBB_object(self, obj):
|
||||
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -932,7 +953,7 @@ class QtoCalculator:
|
||||
|
||||
return new_OBB_object
|
||||
|
||||
def get_AABB_object(self, obj):
|
||||
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -988,12 +1009,12 @@ class QtoCalculator:
|
||||
|
||||
def get_bisected_obj(
|
||||
self,
|
||||
obj,
|
||||
plane_co_pos,
|
||||
plane_no_pos,
|
||||
plane_co_neg,
|
||||
plane_no_neg,
|
||||
):
|
||||
obj: bpy.types.Object,
|
||||
plane_co_pos: VectorTuple,
|
||||
plane_no_pos: VectorTuple,
|
||||
plane_co_neg: VectorTuple,
|
||||
plane_no_neg: VectorTuple,
|
||||
) -> bpy.types.Object:
|
||||
"""_summary_: Returns the object bisected by two planes.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1031,11 +1052,12 @@ class QtoCalculator:
|
||||
|
||||
return bis_obj
|
||||
|
||||
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
|
||||
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
|
||||
"""_summary_: Returns the total contact area of the object with other objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return float: Total contact area of the object with other objects.
|
||||
"""
|
||||
total_contact_area = 0
|
||||
@@ -1046,11 +1068,12 @@ class QtoCalculator:
|
||||
|
||||
return total_contact_area
|
||||
|
||||
def get_touching_objects(self, obj, class_filter):
|
||||
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
|
||||
"""_summary_: Returns a list of objects that are touching the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return list: List of touching objects
|
||||
"""
|
||||
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
|
||||
@@ -1094,7 +1117,7 @@ class QtoCalculator:
|
||||
|
||||
return touching_objects
|
||||
|
||||
def get_contact_area(self, object1, object2):
|
||||
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the contact area between two objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1109,7 +1132,13 @@ class QtoCalculator:
|
||||
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
|
||||
return total_area
|
||||
|
||||
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
|
||||
def get_intersection_between_polygons(
|
||||
self,
|
||||
object1: bpy.types.Object,
|
||||
poly1: bpy.types.MeshPolygon,
|
||||
object2: bpy.types.Object,
|
||||
poly2: bpy.types.MeshPolygon,
|
||||
) -> float:
|
||||
"""_summary_: Returns the intersection between two polygons.
|
||||
|
||||
:param blender-object object1: Blender Object
|
||||
@@ -1152,7 +1181,9 @@ class QtoCalculator:
|
||||
# TopologicalError - Generated Geometry might be invalid
|
||||
return 0
|
||||
|
||||
def create_shapely_polygon(self, obj, polygon, trans_matrix):
|
||||
def create_shapely_polygon(
|
||||
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
|
||||
) -> Polygon:
|
||||
"""_summary_: Create a shapely polygon
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1171,12 +1202,14 @@ class QtoCalculator:
|
||||
polygon_tuples.append((x, y))
|
||||
return Polygon(polygon_tuples)
|
||||
|
||||
def get_gross_element_mesh(self, element):
|
||||
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
|
||||
return self.create_mesh_from_shape(element, settings)
|
||||
|
||||
def create_mesh_from_shape(self, element, settings=None):
|
||||
def create_mesh_from_shape(
|
||||
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
|
||||
) -> bpy.types.Mesh:
|
||||
if settings is None:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
@@ -1203,12 +1236,12 @@ class QtoCalculator:
|
||||
mesh.update()
|
||||
return mesh
|
||||
|
||||
def get_bmesh_from_mesh(self, mesh):
|
||||
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
return bm
|
||||
|
||||
def get_object_main_axis(self, o):
|
||||
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
|
||||
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -1227,18 +1260,18 @@ class QtoCalculator:
|
||||
else:
|
||||
return "x"
|
||||
|
||||
def is_opening_horizontal(self, o):
|
||||
def is_opening_horizontal(self, o: bpy.types.Object) -> bool:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
return z < x and z < y
|
||||
|
||||
def delete_mesh(self, mesh):
|
||||
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
|
||||
mesh.user_clear()
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
def delete_obj(self, obj):
|
||||
def delete_obj(self, obj: bpy.types.Object) -> None:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ classes = (
|
||||
operator.RemovePropTemplate,
|
||||
operator.RemovePsetTemplate,
|
||||
operator.RemovePsetTemplateFile,
|
||||
operator.SavePsetTemplateFile,
|
||||
prop.PsetTemplate,
|
||||
prop.EnumerationValues,
|
||||
prop.PropTemplate,
|
||||
|
||||
@@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
bl_idname = "bim.unlink_object"
|
||||
bl_label = "Unlink Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Object Name")
|
||||
should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -227,6 +227,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
|
||||
objects: list[bpy.types.Object]
|
||||
for obj in objects:
|
||||
was_active_object = obj == context.active_object
|
||||
|
||||
@@ -242,6 +243,27 @@ class UnlinkObject(bpy.types.Operator):
|
||||
if obj.data:
|
||||
obj_copy.data = obj.data.copy()
|
||||
|
||||
# prevent unlinking materials that might be used elsewhere
|
||||
replacements: dict[bpy.types.Material, bpy.types.Material] = dict()
|
||||
for material_slot in obj_copy.material_slots:
|
||||
material = material_slot.material
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
if material in replacements:
|
||||
material_replacement = replacements[material]
|
||||
|
||||
# no need to copy non-ifc materials as unlinking won't do anything to them
|
||||
elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None:
|
||||
replacements[material] = material
|
||||
continue
|
||||
|
||||
else:
|
||||
material_replacement = material.copy()
|
||||
replacements[material] = material_replacement
|
||||
|
||||
material_slot.material = material_replacement
|
||||
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
|
||||
obj = obj_copy
|
||||
|
||||
@@ -153,6 +153,17 @@ class Bsdd:
|
||||
def should_load_preview_domains(cls): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Clash:
|
||||
def export_clash_sets(cls): pass
|
||||
def get_clash(cls, clash_set, a_global_id, b_global_id): pass
|
||||
def get_clash_set(cls, name): pass
|
||||
def get_clash_sets(cls): pass
|
||||
def import_active_clashes(cls): pass
|
||||
def load_clash_sets(cls, fn): pass
|
||||
def look_at(cls, target, location): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Collector:
|
||||
def assign(cls, obj): pass
|
||||
|
||||
@@ -22,6 +22,7 @@ from blenderbim.tool.boundary import Boundary
|
||||
from blenderbim.tool.brick import Brick
|
||||
from blenderbim.tool.bsdd import Bsdd
|
||||
from blenderbim.tool.cad import Cad
|
||||
from blenderbim.tool.clash import Clash
|
||||
from blenderbim.tool.collector import Collector
|
||||
from blenderbim.tool.context import Context
|
||||
from blenderbim.tool.debug import Debug
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
from contextlib import contextmanager
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
class Clash(blenderbim.core.tool.Clash):
|
||||
|
||||
@classmethod
|
||||
def export_clash_sets(cls):
|
||||
clash_sets = []
|
||||
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets:
|
||||
a = []
|
||||
b = []
|
||||
for ab in ["a", "b"]:
|
||||
for data in getattr(clash_set, ab):
|
||||
clash_source = {"file": data.name}
|
||||
if data.selector:
|
||||
clash_source["selector"] = data.selector
|
||||
clash_source["mode"] = data.mode
|
||||
if ab == "a":
|
||||
a.append(clash_source)
|
||||
elif ab == "b":
|
||||
b.append(clash_source)
|
||||
clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b}
|
||||
if clash_set.mode == "intersection":
|
||||
clash_set_data["tolerance"] = clash_set.tolerance
|
||||
clash_set_data["check_all"] = clash_set.check_all
|
||||
elif clash_set.mode == "collision":
|
||||
clash_set_data["allow_touching"] = clash_set.allow_touching
|
||||
elif clash_set.mode == "clearance":
|
||||
clash_set_data["clearance"] = clash_set.clearance
|
||||
clash_set_data["check_all"] = clash_set.check_all
|
||||
clash_sets.append(clash_set_data)
|
||||
return clash_sets
|
||||
|
||||
@classmethod
|
||||
def get_clash(cls, clash_set, a_global_id, b_global_id):
|
||||
clashes = clash_set.get("clashes", None)
|
||||
if not clashes:
|
||||
return
|
||||
return clashes.get(f"{a_global_id}-{b_global_id}", None)
|
||||
|
||||
@classmethod
|
||||
def get_clash_set(cls, name):
|
||||
for clash_set in ClashStore.clash_sets:
|
||||
if clash_set["name"] == name:
|
||||
return clash_set
|
||||
|
||||
@classmethod
|
||||
def get_clash_sets(cls):
|
||||
return ClashStore.clash_sets
|
||||
|
||||
@classmethod
|
||||
def import_active_clashes(cls):
|
||||
clash_set = bpy.context.scene.BIMClashProperties.active_clash_set
|
||||
if not clash_set:
|
||||
return
|
||||
clash_set.clashes.clear()
|
||||
result = tool.Clash.get_clash_set(clash_set.name)
|
||||
for clash in result.get("clashes", {}).values():
|
||||
blender_clash = clash_set.clashes.add()
|
||||
blender_clash.a_global_id = clash["a_global_id"]
|
||||
blender_clash.b_global_id = clash["b_global_id"]
|
||||
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
||||
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
||||
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
|
||||
|
||||
@classmethod
|
||||
def load_clash_sets(cls, fn):
|
||||
with open(fn) as f:
|
||||
ClashStore.clash_sets = json.load(f)
|
||||
|
||||
@classmethod
|
||||
def look_at(cls, target, location):
|
||||
camera_location = location
|
||||
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
|
||||
region = next(region for region in area.regions if region.type == "WINDOW")
|
||||
space = next(space for space in area.spaces if space.type == "VIEW_3D")
|
||||
override = {"area": area, "region": region, "space_data": space}
|
||||
space.region_3d.view_location = target
|
||||
space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y")
|
||||
space.region_3d.view_distance = (camera_location - target).length
|
||||
space.shading.show_xray = True
|
||||
|
||||
|
||||
class ClashStore:
|
||||
clash_sets = None
|
||||
path = None
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
ClashStore.clash_sets = None
|
||||
ClashStore.path = None
|
||||
@@ -79,7 +79,7 @@ class Ifc(blenderbim.core.tool.Ifc):
|
||||
return IfcStore.get_schema()
|
||||
|
||||
@classmethod
|
||||
def get_entity(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
|
||||
def get_entity(cls, obj: IFC_CONNECTED_TYPE) -> ifcopenshell.entity_instance:
|
||||
ifc = IfcStore.get_file()
|
||||
props = getattr(obj, "BIMObjectProperties", None)
|
||||
if ifc and props and props.ifc_definition_id:
|
||||
|
||||
@@ -23,14 +23,17 @@ import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
from mathutils import Vector
|
||||
from ifcopenshell import util
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim.schema
|
||||
from typing import Optional, Union, Literal
|
||||
|
||||
|
||||
class Qto(blenderbim.core.tool.Qto):
|
||||
@classmethod
|
||||
def get_radius_of_selected_vertices(cls, obj):
|
||||
def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float:
|
||||
selected_verts = [v.co for v in obj.data.vertices if v.select]
|
||||
total = Vector()
|
||||
for v in selected_verts:
|
||||
@@ -39,16 +42,16 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return max([(v - circle_center).length for v in selected_verts])
|
||||
|
||||
@classmethod
|
||||
def set_qto_result(cls, result):
|
||||
def set_qto_result(cls, result: float) -> None:
|
||||
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
|
||||
|
||||
@classmethod
|
||||
def add_object_base_qto(cls, obj):
|
||||
def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
return cls.add_product_base_qto(product)
|
||||
|
||||
@classmethod
|
||||
def add_product_base_qto(cls, product):
|
||||
def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
base_quantity_name = cls.get_applicable_base_quantity_name(product)
|
||||
if base_quantity_name:
|
||||
return tool.Ifc.run(
|
||||
@@ -58,7 +61,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_quantity_names(cls, qto_name):
|
||||
def get_applicable_quantity_names(cls, qto_name: str) -> list[str]:
|
||||
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name)
|
||||
return (
|
||||
[property.Name for property in pset_template.HasPropertyTemplates]
|
||||
@@ -67,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_base_quantity_name(cls, product=None):
|
||||
def get_applicable_base_quantity_name(
|
||||
cls, product: Optional[ifcopenshell.entity_instance] = None
|
||||
) -> Union[str, None]:
|
||||
if not product:
|
||||
return
|
||||
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
|
||||
@@ -76,36 +81,44 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
|
||||
|
||||
@classmethod
|
||||
def get_new_calculated_quantity(cls, qto_name, quantity_name, obj):
|
||||
def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj)
|
||||
|
||||
@classmethod
|
||||
def get_new_guessed_quantity(cls, obj, quantity_name, alternative_prop_names):
|
||||
def get_new_guessed_quantity(
|
||||
cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str]
|
||||
) -> Union[float, None]:
|
||||
return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj)
|
||||
|
||||
@classmethod
|
||||
def get_rounded_value(cls, new_quantity):
|
||||
def get_rounded_value(cls, new_quantity: float) -> float:
|
||||
return round(new_quantity, 3)
|
||||
|
||||
@classmethod
|
||||
def get_calculated_object_quantities(cls, calculator, qto_name, obj):
|
||||
def get_calculated_object_quantities(
|
||||
cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object
|
||||
) -> dict[str, float]:
|
||||
return {
|
||||
quantity_name: cls.get_rounded_value(calculator.calculate_quantity(qto_name, quantity_name, obj))
|
||||
quantity_name: cls.get_rounded_value(value)
|
||||
for quantity_name in cls.get_applicable_quantity_names(qto_name) or []
|
||||
if cls.has_calculator(qto_name, quantity_name)
|
||||
and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None
|
||||
and (value := calculator.calculate_quantity(qto_name, quantity_name, obj)) is not None
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def has_calculator(cls, qto_name, quantity_name):
|
||||
def has_calculator(cls, qto_name: str, quantity_name: str) -> bool:
|
||||
return bool(mapper.get(qto_name, {}).get(quantity_name, None))
|
||||
|
||||
@classmethod
|
||||
def convert_to_project_units(cls, value, qto_name=None, quantity_name=None, quantity_type=None):
|
||||
def convert_to_project_units(
|
||||
cls,
|
||||
value: float,
|
||||
qto_name: Optional[str] = None,
|
||||
quantity_name: Optional[str] = None,
|
||||
quantity_type: Optional[QuanityTypes] = None,
|
||||
) -> Union[float, None]:
|
||||
"""You can either specify `quantity_type` or provide `qto_name/quantity_name`
|
||||
to let method figure the `quantity_type` from the templates
|
||||
|
||||
`quantity_type` values are `Q_LENGTH`, `Q_AREA`, `Q_VOLUME`
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
quantity_to_unit_types = {
|
||||
@@ -135,7 +148,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_guessed_quantities(cls, obj, pset_qto_properties):
|
||||
def get_guessed_quantities(
|
||||
cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance]
|
||||
) -> dict[str, float]:
|
||||
calculated_quantities = {}
|
||||
for pset_qto_property in pset_qto_properties:
|
||||
quantity_name = pset_qto_property.get_info()["Name"]
|
||||
@@ -153,7 +168,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return calculated_quantities
|
||||
|
||||
@classmethod
|
||||
def get_base_qto(cls, product):
|
||||
def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not hasattr(product, "IsDefinedBy"):
|
||||
return
|
||||
for rel in product.IsDefinedBy or []:
|
||||
@@ -166,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
@classmethod
|
||||
def get_related_cost_item_quantities(cls, product):
|
||||
def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]:
|
||||
"""_summary_: Returns the related cost item and related quantities of the product
|
||||
|
||||
:param ifc-instance product: ifc instance
|
||||
|
||||
@@ -21,6 +21,8 @@ import ifcopenshell
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.material
|
||||
import blenderbim.core.style
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
@@ -150,9 +150,9 @@ class Spatial(blenderbim.core.tool.Spatial):
|
||||
for product in products:
|
||||
obj = tool.Ifc.get_object(product)
|
||||
if obj and bpy.context.view_layer.objects.get(obj.name):
|
||||
obj.select_set(True)
|
||||
if unhide:
|
||||
obj.hide_set(False)
|
||||
obj.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def filter_products(cls, products, action):
|
||||
|
||||
@@ -96,7 +96,7 @@ class Style(blenderbim.core.tool.Style):
|
||||
return obj.name
|
||||
|
||||
@classmethod
|
||||
def get_style(cls, obj):
|
||||
def get_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if obj.BIMMaterialProperties.ifc_style_id:
|
||||
try:
|
||||
return tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,138 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# When run from Blender
|
||||
BLEND_DIR = os.path.dirname(bpy.data.filepath)
|
||||
OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Entourage Library.ifc")
|
||||
|
||||
|
||||
class LibraryGenerator:
|
||||
def generate(self, library_name, output_filename):
|
||||
ifcopenshell.api.pre_listeners = {}
|
||||
ifcopenshell.api.post_listeners = {}
|
||||
|
||||
self.materials = {}
|
||||
|
||||
self.file = ifcopenshell.api.run("project.create_file")
|
||||
self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name)
|
||||
self.library = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
|
||||
)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
|
||||
|
||||
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
|
||||
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
|
||||
self.representations = {
|
||||
"Model/Body/MODEL_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="MODEL_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"Plan/Body/PLAN_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Plan",
|
||||
context_identifier="Body",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=plan,
|
||||
),
|
||||
"Model/Body/PLAN_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"Model/Body/SECTION_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="SECTION_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
}
|
||||
|
||||
# Manually modeled trees
|
||||
for obj in bpy.data.objects:
|
||||
if not obj.type == "MESH":
|
||||
continue
|
||||
if "Plan/" in obj.name or "Model/" in obj.name:
|
||||
continue
|
||||
representations = {"Model/Body/MODEL_VIEW": obj.name}
|
||||
for rep_key in self.representations.keys():
|
||||
rep_obj = bpy.data.objects.get(obj.name + " " + rep_key)
|
||||
if rep_obj:
|
||||
representations[rep_key] = rep_obj.name
|
||||
self.create_type("IfcBuildingElementProxyType", obj.name, representations)
|
||||
|
||||
self.file.write(output_filename)
|
||||
|
||||
def create_type(self, ifc_class, name, representations):
|
||||
element = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class=ifc_class, predefined_type="ENTOURAGE", name=name
|
||||
)
|
||||
for rep_name, obj_name in representations.items():
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_representation",
|
||||
self.file,
|
||||
context=self.representations[rep_name],
|
||||
blender_object=obj,
|
||||
geometry=obj.data,
|
||||
total_items=max(1, len(obj.material_slots)),
|
||||
)
|
||||
styles = []
|
||||
for slot in obj.material_slots:
|
||||
style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
|
||||
ifcopenshell.api.run(
|
||||
"style.add_surface_style",
|
||||
self.file,
|
||||
style=style,
|
||||
ifc_class="IfcSurfaceStyleShading",
|
||||
attributes=tool.Style.get_surface_shading_attributes(slot.material),
|
||||
)
|
||||
styles.append(style)
|
||||
if styles:
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", self.file, product=element, representation=representation
|
||||
)
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
LibraryGenerator().generate("Entourage Assets Library", output_filename=OUT_PATH)
|
||||
@@ -438,7 +438,7 @@ Scenario: Override duplicate move - copying an aggregate
|
||||
And the object "IfcElementAssembly/Assembly.001" exists
|
||||
And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001"
|
||||
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey"
|
||||
|
||||
|
||||
Scenario: Override duplicate move - copying objects with connection
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
@@ -467,7 +467,7 @@ Scenario: Override duplicate move - copying objects with connection
|
||||
Then the object "IfcSlab/Slab.001" exists
|
||||
And the variable "slab_name" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcSlab/Slab.001'][0]"
|
||||
Then the object "{wall_name}" has a connection with "{slab_name}"
|
||||
|
||||
|
||||
Scenario: Override duplicate move - copying walls with mitre joint
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
@@ -477,24 +477,24 @@ Scenario: Override duplicate move - copying walls with mitre joint
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
When I deselect all objects
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I duplicate the selected objects
|
||||
Then the object "IfcWall/Wall.002" exists
|
||||
And the variable "wall_name1" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.002'][0]"
|
||||
Then the object "IfcWall/Wall.003" exists
|
||||
And the variable "wall_name2" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.003'][0]"
|
||||
Then the object "{wall_name1}" has a connection with "{wall_name2}"
|
||||
|
||||
|
||||
Scenario: Override duplicate move linked - without active IFC data
|
||||
Given an empty Blender session
|
||||
And I add a cube
|
||||
@@ -544,14 +544,14 @@ Scenario: Override paste buffer - with active IFC data
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
When I press "view3d.copybuffer"
|
||||
# IFC elements unlinked on paste for safety
|
||||
And I press "bim.override_paste_buffer"
|
||||
Then the object "IfcWall/Cube" exists
|
||||
And the object "IfcWall/Cube" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" exists
|
||||
And the object "IfcWall/Cube.001" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
And the object "IfcBuildingStorey/My Storey.001" exists
|
||||
And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey"
|
||||
And the object "Cube.001" exists
|
||||
And the object "Cube.001" is not an IFC element
|
||||
And the object "My Storey.001" exists
|
||||
And the object "My Storey.001" is not an IFC element
|
||||
|
||||
Scenario: Duplicate linked aggregate
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
@@ -559,8 +559,8 @@ Scenario: Duplicate linked aggregate
|
||||
When I duplicate linked aggregate the selected objects
|
||||
Then the object "IfcWall/Wall_01.001" exists
|
||||
And the object "IfcWall/Wall_02.001" exists
|
||||
And the object "IfcElementAssembly/Assembly_02" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_02" belong to the same Linked Aggregate Group
|
||||
And the object "IfcElementAssembly/Assembly_01" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_01" belong to the same Linked Aggregate Group
|
||||
|
||||
Scenario: Refresh linked aggregate
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
@@ -588,7 +588,7 @@ Scenario: Refresh linked aggregate - after deleting an object
|
||||
When I refresh linked aggregate the selected object
|
||||
Then the object "IfcWall/Wall_01" does not exist
|
||||
And the object "IfcWall/Wall_02" exists
|
||||
|
||||
|
||||
Scenario: Refresh linked aggregate - after duplicating an object
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
And the object "IfcWall/Wall_01" is selected
|
||||
@@ -606,4 +606,4 @@ Scenario: Refresh linked aggregate - after duplicating an object
|
||||
Then the object "IfcWall/Wall_01.001" exists
|
||||
And the object "IfcWall/Wall_02.001" exists
|
||||
And the object "IfcWall/Wall_03.001" exists
|
||||
|
||||
|
||||
|
||||
@@ -107,11 +107,11 @@ Scenario: Add a wall perpendicular to an existing wall
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
When I press "bim.hotkey(hotkey='S_A')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Extend one wall to another
|
||||
Given an empty IFC project
|
||||
@@ -122,35 +122,17 @@ Scenario: Extend one wall to another
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_E')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-2,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-2,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - first wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -162,13 +144,13 @@ Scenario: Join two walls with a butt joint - second wall has priority
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -179,12 +161,30 @@ Scenario: Join two walls with a mitre joint
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Change the height of a wall
|
||||
Given an empty IFC project
|
||||
|
||||
@@ -387,10 +387,10 @@ Scenario: Edit pset length property
|
||||
|
||||
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
|
||||
Scenario: Edit qset length property
|
||||
Given an empty IFC project
|
||||
And I press "mesh.add_clever_stair"
|
||||
@@ -401,15 +401,14 @@ Scenario: Edit qset length property
|
||||
|
||||
# Testing Q_LENGTH type of prop
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57495"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
@@ -24,7 +24,7 @@ Scenario: Unlink object
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material"
|
||||
And I press "bim.add_style()"
|
||||
When I press "bim.unlink_object(obj='IfcWall/Cube')"
|
||||
Then the object "Cube" is not an IFC element
|
||||
And the material "Material" is an IFC style
|
||||
|
||||
@@ -18,7 +18,7 @@ Scenario: Remove style
|
||||
When I press "bim.remove_style(style={style})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Add style
|
||||
Scenario: Add style to current blender material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
@@ -33,32 +33,39 @@ Scenario: Unlink style
|
||||
When I press "bim.unlink_style"
|
||||
Then the material "Material" is not an IFC style
|
||||
|
||||
Scenario: Enable editing style
|
||||
Scenario: Add a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
When I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
When I press "bim.add_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Scenario: Disable adding a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
When I press "bim.disable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
When I press "bim.disable_adding_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Edit style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.edit_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Given an empty IFC project
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.disable_editing_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Load styles
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
|
||||
@@ -51,7 +51,7 @@ def replace_variables(value):
|
||||
|
||||
|
||||
def is_x(number, x):
|
||||
return abs(number - x) < 1e-6
|
||||
return abs(number - x) < 1e-5
|
||||
|
||||
|
||||
def vectors_are_equal(v1, v2):
|
||||
|
||||
@@ -135,8 +135,7 @@ class Clasher:
|
||||
self.logger.info(f"Adding objects {name}")
|
||||
assert iterator.initialize()
|
||||
while True:
|
||||
self.tree.add_element(iterator.get_native(), should_triangulate=True)
|
||||
# self.tree.add_element(iterator.get())
|
||||
self.tree.add_element(iterator.get())
|
||||
shape = iterator.get()
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
@@ -28,7 +28,19 @@
|
||||
bool IfcGeom::Kernel::convert(const IfcSchema::IfcDerivedProfileDef* l, TopoDS_Shape& face) {
|
||||
TopoDS_Face f;
|
||||
gp_Trsf2d trsf2d;
|
||||
if (convert_face(l->ParentProfile(), f) && IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
bool is_mirror = false;
|
||||
#ifdef SCHEMA_HAS_IfcMirroredProfileDef
|
||||
if (l->as<IfcSchema::IfcMirroredProfileDef>()) {
|
||||
trsf2d.SetMirror(gp::Origin2d());
|
||||
is_mirror = true;
|
||||
}
|
||||
#endif
|
||||
if (!is_mirror) {
|
||||
if (!IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (convert_face(l->ParentProfile(), f)) {
|
||||
gp_Trsf trsf = trsf2d;
|
||||
face = BRepBuilderAPI_Transform(f, trsf).Shape();
|
||||
return true;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,588 @@
|
||||
#include "clash_utils.h"
|
||||
#include <cassert>
|
||||
|
||||
#define GU_CULLING_EPSILON_RAY_TRIANGLE FLT_EPSILON*FLT_EPSILON
|
||||
#define PX_MAX_F32 3.4028234663852885981170418348452e+38F
|
||||
|
||||
typedef uint32_t PxU32;
|
||||
|
||||
// Why can't I use std::clamp?
|
||||
template<typename TC>
|
||||
const TC& ios_clamp(const TC& v, const TC& lo, const TC& hi) {
|
||||
assert(!(hi < lo));
|
||||
return (v < lo) ? lo : (hi < v) ? hi : v;
|
||||
}
|
||||
|
||||
// Branchless slab method. Note that this can still be optimised further by batching boxes.
|
||||
// From Tavian Barnes - MIT License
|
||||
// https://tavianator.com/2022/ray_box_boundary.html
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
|
||||
float tmin = 0.0, tmax = INFINITY;
|
||||
|
||||
for (int d = 0; d < 3; ++d) {
|
||||
bool sign = std::signbit(ray->dir_inv[d]);
|
||||
float bmin = box->corners[sign][d];
|
||||
float bmax = box->corners[!sign][d];
|
||||
|
||||
float dmin = (bmin - ray->origin[d]) * ray->dir_inv[d];
|
||||
float dmax = (bmax - ray->origin[d]) * ray->dir_inv[d];
|
||||
|
||||
tmin = std::max(dmin, tmin);
|
||||
tmax = std::min(dmax, tmax);
|
||||
}
|
||||
|
||||
return tmin < tmax;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge) {
|
||||
// Find vectors for two edges sharing vert0
|
||||
const gp_Vec edge1 = vert1 - vert0;
|
||||
const gp_Vec edge2 = vert2 - vert0;
|
||||
|
||||
// Begin calculating determinant - also used to calculate U parameter
|
||||
const gp_Vec pvec = dir.Crossed(edge2); // error ~ |v2-v0|
|
||||
|
||||
// If determinant is near zero, ray lies in plane of triangle
|
||||
const Standard_Real det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0|
|
||||
|
||||
if(cull)
|
||||
{
|
||||
if(det<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0;
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec);
|
||||
|
||||
const Standard_Real enlargeCoeff = enlarge*det;
|
||||
const Standard_Real uvlimit = -enlargeCoeff;
|
||||
const Standard_Real uvlimit2 = det + enlargeCoeff;
|
||||
|
||||
if(u<uvlimit || u>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec);
|
||||
if(v<uvlimit || (u+v)>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Calculate t, scale parameters, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec);
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
at = t*inv_det;
|
||||
au = u*inv_det;
|
||||
av = v*inv_det;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the non-culling branch
|
||||
if(std::abs(det)<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0; // error ~ |orig-v0|
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec) * inv_det;
|
||||
if(u<-enlarge || u>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec) * inv_det;
|
||||
if(v<-enlarge || (u+v)>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// Calculate t, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec) * inv_det;
|
||||
|
||||
at = t;
|
||||
au = u;
|
||||
av = v;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/sweep/GuSweepCapsuleCapsule.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b) // seg 2 origin, vector
|
||||
{
|
||||
const gp_Vec Tx = q - p;
|
||||
const double ADotA = a.Dot(a);
|
||||
const double BDotB = b.Dot(b);
|
||||
const double ADotB = a.Dot(b);
|
||||
const double ADotT = a.Dot(Tx);
|
||||
const double BDotT = b.Dot(Tx);
|
||||
|
||||
// t parameterizes ray (p, a)
|
||||
// u parameterizes ray (q, b)
|
||||
|
||||
// Compute t for the closest point on ray (p, a) to ray (q, b)
|
||||
const Standard_Real Denom = ADotA*BDotB - ADotB*ADotB;
|
||||
|
||||
Standard_Real t; // We will clamp result so t is on the segment (p, a)
|
||||
if(Denom!=0.0f)
|
||||
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
|
||||
// find u for point on ray (q, b) closest to point at t
|
||||
Standard_Real u;
|
||||
if(BDotB!=0.0f)
|
||||
{
|
||||
u = (t*ADotB - BDotT) / BDotB;
|
||||
|
||||
// if u is on segment (q, b), t and u correspond to closest points, otherwise, clamp u, recompute and clamp t
|
||||
if(u<0.0f)
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
else if(u > 1.0f)
|
||||
{
|
||||
u = 1.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp((ADotB + ADotT) / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
|
||||
x = p + a * t;
|
||||
y = q + b * u;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/distance/GuDistanceTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q)
|
||||
{
|
||||
std::array<gp_Vec, 3> Sv;
|
||||
Sv[0] = p[1] - p[0];
|
||||
Sv[1] = p[2] - p[1];
|
||||
Sv[2] = p[0] - p[2];
|
||||
|
||||
std::array<gp_Vec, 3> Tv;
|
||||
Tv[0] = q[1] - q[0];
|
||||
Tv[1] = q[2] - q[1];
|
||||
Tv[2] = q[0] - q[2];
|
||||
|
||||
gp_Vec minP, minQ;
|
||||
bool shown_disjoint = false;
|
||||
|
||||
float mindd = PX_MAX_F32;
|
||||
|
||||
for(int i=0;i<3;i++)
|
||||
{
|
||||
for(int j=0;j<3;j++)
|
||||
{
|
||||
edgeEdgeDist(cp, cq, p[i], Sv[i], q[j], Tv[j]);
|
||||
const gp_Vec V = cq - cp;
|
||||
const float dd = V.Dot(V);
|
||||
|
||||
if(dd<=mindd)
|
||||
{
|
||||
minP = cp;
|
||||
minQ = cq;
|
||||
mindd = dd;
|
||||
|
||||
int id = i+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
gp_Vec Z = p[id] - cp;
|
||||
float a = Z.Dot(V);
|
||||
id = j+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
Z = q[id] - cq;
|
||||
float b = Z.Dot(V);
|
||||
|
||||
if((a<=0.0f) && (b>=0.0f))
|
||||
return V.Dot(V);
|
||||
|
||||
if(a<=0.0f) a = 0.0f;
|
||||
else if(b>0.0f) b = 0.0f;
|
||||
|
||||
if((mindd - a + b) > 0.0f)
|
||||
shown_disjoint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Sn = Sv[0].Crossed(Sv[1]);
|
||||
float Snl = Sn.Dot(Sn);
|
||||
|
||||
if(Snl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Tp = {(p[0] - q[0]).Dot(Sn),
|
||||
(p[0] - q[1]).Dot(Sn),
|
||||
(p[0] - q[2]).Dot(Sn)};
|
||||
|
||||
int index = -1;
|
||||
if((Tp[0]>0.0f) && (Tp[1]>0.0f) && (Tp[2]>0.0f))
|
||||
{
|
||||
if(Tp[0]<Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]<Tp[index]) index = 2;
|
||||
}
|
||||
else if((Tp[0]<0.0f) && (Tp[1]<0.0f) && (Tp[2]<0.0f))
|
||||
{
|
||||
if(Tp[0]>Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]>Tp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& qIndex = q[index];
|
||||
|
||||
gp_Vec V = qIndex - p[0];
|
||||
gp_Vec Z = Sn.Crossed(Sv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[1];
|
||||
Z = Sn.Crossed(Sv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[2];
|
||||
Z = Sn.Crossed(Sv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = qIndex + Sn * Tp[index]/Snl;
|
||||
cq = qIndex;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
|
||||
float Tnl = Tn.Dot(Tn);
|
||||
|
||||
if(Tnl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Sp = {(q[0] - p[0]).Dot(Tn),
|
||||
(q[0] - p[1]).Dot(Tn),
|
||||
(q[0] - p[2]).Dot(Tn)};
|
||||
|
||||
int index = -1;
|
||||
if((Sp[0]>0.0f) && (Sp[1]>0.0f) && (Sp[2]>0.0f))
|
||||
{
|
||||
if(Sp[0]<Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]<Sp[index]) index = 2;
|
||||
}
|
||||
else if((Sp[0]<0.0f) && (Sp[1]<0.0f) && (Sp[2]<0.0f))
|
||||
{
|
||||
if(Sp[0]>Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]>Sp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& pIndex = p[index];
|
||||
|
||||
gp_Vec V = pIndex - q[0];
|
||||
gp_Vec Z = Tn.Crossed(Tv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[1];
|
||||
Z = Tn.Crossed(Tv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[2];
|
||||
Z = Tn.Crossed(Tv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = pIndex;
|
||||
cq = pIndex + Tn * Sp[index]/Tnl;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(shown_disjoint)
|
||||
{
|
||||
cp = minP;
|
||||
cq = minQ;
|
||||
return mindd;
|
||||
}
|
||||
else return 0.0f;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
//Based on the paper A Fast Triangle-Triangle Intersection Test by T. Moeller
|
||||
//http://web.stanford.edu/class/cs277/resources/papers/Moller1997b.pdf
|
||||
namespace {
|
||||
struct Interval
|
||||
{
|
||||
Standard_Real min;
|
||||
Standard_Real max;
|
||||
gp_Vec minPoint;
|
||||
gp_Vec maxPoint;
|
||||
|
||||
Interval() : min(FLT_MAX), max(-FLT_MAX), minPoint(gp_Vec(NAN, NAN, NAN)), maxPoint(gp_Vec(NAN, NAN, NAN)) { }
|
||||
|
||||
static bool overlapOrTouch(const Interval& a, const Interval& b)
|
||||
{
|
||||
return !(a.min > b.max || b.min > a.max);
|
||||
}
|
||||
|
||||
static Interval intersection(const Interval& a, const Interval& b)
|
||||
{
|
||||
Interval result;
|
||||
if (!overlapOrTouch(a, b))
|
||||
return result;
|
||||
|
||||
if (a.min > b.min) {
|
||||
result.min = a.min;
|
||||
result.minPoint = a.minPoint;
|
||||
} else {
|
||||
result.min = b.min;
|
||||
result.minPoint = b.minPoint;
|
||||
}
|
||||
|
||||
if (a.max < b.max) {
|
||||
result.max = a.max;
|
||||
result.maxPoint = a.maxPoint;
|
||||
} else {
|
||||
result.max = b.max;
|
||||
result.maxPoint = b.maxPoint;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void include(Standard_Real d, const gp_Vec& p)
|
||||
{
|
||||
if (d < min) { min = d; minPoint = p; }
|
||||
if (d > max) { max = d; maxPoint = p; }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB, Standard_Real distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir)
|
||||
{
|
||||
Interval i;
|
||||
|
||||
const bool bA = distanceA > 0;
|
||||
const bool bB = distanceB > 0;
|
||||
const bool bC = distanceC > 0;
|
||||
distanceA = std::abs(distanceA);
|
||||
distanceB = std::abs(distanceB);
|
||||
distanceC = std::abs(distanceC);
|
||||
|
||||
if (bA != bB)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceB)) * b + (distanceB / (distanceA + distanceB)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bA != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceC)) * c + (distanceC / (distanceA + distanceC)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bB != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceB / (distanceB + distanceC)) * c + (distanceC / (distanceB + distanceC)) * b;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y)
|
||||
{
|
||||
return (a.Coord(y) - c.Coord(y)) * (b.Coord(x) - c.Coord(x)) - (a.Coord(x) - c.Coord(x)) * (b.Coord(y) - c.Coord(y));
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real ab = orient2d(a, b, point, x, y);
|
||||
const Standard_Real bc = orient2d(b, c, point, x, y);
|
||||
const Standard_Real ca = orient2d(c, a, point, x, y);
|
||||
|
||||
if ((ab >= 0) == (bc >= 0) && (ab >= 0) == (ca >= 0))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real aaS = orient2d(startA, endA, startB, x, y);
|
||||
const Standard_Real aaE = orient2d(startA, endA, endB, x, y);
|
||||
|
||||
if ((aaS >= 0) == (aaE >= 0))
|
||||
return false;
|
||||
|
||||
const Standard_Real bbS = orient2d(startB, endB, startA, x, y);
|
||||
const Standard_Real bbE = orient2d(startB, endB, endA, x, y);
|
||||
|
||||
if ((bbS >= 0) == (bbE >= 0))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void getProjectionIndices(gp_Vec normal, PxU32& x, PxU32& y)
|
||||
{
|
||||
normal.SetCoord(std::abs(normal.X()), std::abs(normal.Y()), std::abs(normal.Z()));
|
||||
|
||||
if (normal.X() >= normal.Y() && normal.X() >= normal.Z())
|
||||
{
|
||||
//x is the dominant normal direction
|
||||
x = 1;
|
||||
y = 2;
|
||||
}
|
||||
else if (normal.Y() >= normal.X() && normal.Y() >= normal.Z())
|
||||
{
|
||||
//y is the dominant normal direction
|
||||
x = 2;
|
||||
y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
//z is the dominant normal direction
|
||||
x = 0;
|
||||
y = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2)
|
||||
{
|
||||
PxU32 x = 0;
|
||||
PxU32 y = 0;
|
||||
getProjectionIndices(p1_n, x, y);
|
||||
|
||||
const Standard_Real third = (1.0f / 3.0f);
|
||||
|
||||
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
|
||||
//difference is not very big and the coplanar case is not expected to be the most common case
|
||||
if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) ||
|
||||
linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) ||
|
||||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
|
||||
pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// Also with minor modification to return intersection points.
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar)
|
||||
{
|
||||
const Standard_Real tolerance = 1e-8f;
|
||||
|
||||
gp_Vec p1_n((b1 - a1).Crossed(c1 - a1).Normalized());
|
||||
double p1_d = -a1.Dot(p1_n);
|
||||
// const PxPlane p1(a1, b1, c1);
|
||||
const Standard_Real p1ToA = a2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToB = b2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToC = c2.Dot(p1_n) + p1_d;
|
||||
|
||||
if(std::abs(p1ToA) < tolerance && std::abs(p1ToB) < tolerance &&std::abs(p1ToC) < tolerance)
|
||||
return ignoreCoplanar ? false : trianglesIntersectCoplanar(p1_n, a1, b1, c1, a2, b2, c2); //Coplanar triangles
|
||||
|
||||
if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0))
|
||||
return false; //All points of triangle 2 on same side of triangle 1 -> no intersection
|
||||
|
||||
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
|
||||
double p2_d = -a2.Dot(p2_n);
|
||||
// const PxPlane p2(a2, b2, c2);
|
||||
const Standard_Real p2ToA = a1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToB = b1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToC = c1.Dot(p2_n) + p2_d;
|
||||
|
||||
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
|
||||
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
|
||||
|
||||
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
|
||||
const Standard_Real l2 = intersectionDirection.SquareMagnitude();
|
||||
intersectionDirection *= 1.0f / std::sqrt(l2);
|
||||
|
||||
const Interval i1 = computeInterval(p2ToA, p2ToB, p2ToC, a1, b1, c1, intersectionDirection);
|
||||
const Interval i2 = computeInterval(p1ToA, p1ToB, p1ToC, a2, b2, c2, intersectionDirection);
|
||||
|
||||
if (Interval::overlapOrTouch(i1, i2))
|
||||
{
|
||||
/*if (intersection)
|
||||
{
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
intersection->p0 = i.minPoint;
|
||||
intersection->p1 = i.maxPoint;
|
||||
}*/
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
int1 = i.minPoint;
|
||||
int2 = i.maxPoint;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <gp_Vec.hxx>
|
||||
#include <array>
|
||||
|
||||
struct ray {
|
||||
float origin[3];
|
||||
float dir[3];
|
||||
float dir_inv[3];
|
||||
};
|
||||
|
||||
struct box {
|
||||
float corners[2][3];
|
||||
};
|
||||
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box);
|
||||
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge=0.0f);
|
||||
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b); // seg 2 origin, vector
|
||||
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q);
|
||||
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar);
|
||||
@@ -109,7 +109,7 @@ endif
|
||||
mkdir -p dist/ifcopenshell
|
||||
cp -r ifcopenshell/* dist/ifcopenshell/
|
||||
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9838698-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/
|
||||
ifeq ($(PLATFORM), win)
|
||||
|
||||
@@ -20,11 +20,11 @@ Pre-built packages
|
||||
| build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ |
|
||||
+----------------+----------------+----------------+----------------+------------------+
|
||||
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and run IfcConvert using the command line.
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
Geometry tree
|
||||
=============
|
||||
|
||||
IfcOpenShell includes a utility to build a unbalanced binary tree of geometry
|
||||
and their bounding boxes. After a tree is built, you can efficiently select
|
||||
geometry by specifying a point, radius, or bounding box.
|
||||
IfcOpenShell includes a utility to build trees of geometry and their bounding
|
||||
boxes. Geometry trees can be used to efficiently select geometry or collide
|
||||
geometry with one another.
|
||||
|
||||
.. image:: images/geometry-tree.png
|
||||
|
||||
The most efficient way to build tree is by using the iterator, as shown in the
|
||||
example below:
|
||||
The most efficient way to build a tree is by using the iterator. If the native
|
||||
OpenCASCADE shape is added to the tree, a **UB Tree** is built. Alternatively,
|
||||
if triangulation is added to the tree, a **BVH Tree** is built. The type of
|
||||
tree determines the type of operation you can perform.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -21,27 +23,208 @@ example below:
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
# Use triangulation to build a BVH tree
|
||||
tree.add_element(iterator.get())
|
||||
|
||||
# Alternatively, use this code to build an unbalanced binary tree
|
||||
# tree.add_element(iterator.get_native())
|
||||
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
Once built, there are three methods you can use to select elements in the tree:
|
||||
``select_box``, ``select``, and ``select_ray``.
|
||||
Clashing or selecting geometry from a geometry tree
|
||||
---------------------------------------------------
|
||||
|
||||
``select_box`` lets you query for elements that contain a point or another
|
||||
element. However, it only checks the bounding box of elements instead of their
|
||||
exact geometry. This is the fastest approach and is recommended if you don't
|
||||
need precise geometry selection.
|
||||
With a **BVH Tree**, you can efficiently clash sets of elements with other
|
||||
elements. You can find elements that intersect, collide, or are within a
|
||||
clearance distance threshold of one another. There are three methods you can
|
||||
use to clash elements in the tree. Each function collides one set of elements
|
||||
with another set of elements.
|
||||
|
||||
``select`` lets you query for elements that contain a point, a sphere, or
|
||||
another element. ``select`` is similar to select box, but additionally
|
||||
considers the actual geometry of the object. This is slower but more precise.
|
||||
- `Detecting intersection clashes between elements`_ detects when an element intersects with another
|
||||
element. This is the most common type of clash detection used when
|
||||
coordinating designs. For example, you might want to know if any pipes go
|
||||
through structural columns or beams.
|
||||
- `Detecting collision clashes between elements`_ detects when an element
|
||||
touches another element. It is the fastest type of clash detection but does
|
||||
not consider the distance that an element goes inside another element. This
|
||||
considers surfaces only so it works on non-manifold geometry but will not
|
||||
detect if an element is completely within another element.
|
||||
- `Detecting clearance clashes between elements`_ detects when an element comes
|
||||
near to another element within a clearance threshold. This is the slowest
|
||||
type of clash detection. It works on non-manifold geometry and does not
|
||||
consider inside vs outside. Elements like pipe and ducts with insulation,
|
||||
structural openings, and equipment will typically require clearance checks.
|
||||
|
||||
``select_ray`` lets you query for elements that intersect with a ray.
|
||||
With a **UB Tree**, you can efficiently select geometry by specifying a point,
|
||||
radius, or bounding box. There are three methods you can use to select elements
|
||||
in the tree.
|
||||
|
||||
- `Selecting elements using bounding boxes`_ lets you query for elements that
|
||||
contain a point or another element. However, it only checks the bounding box
|
||||
of elements instead of their exact geometry. This is the fastest approach and
|
||||
is recommended if you don't need precise geometry selection.
|
||||
- `Selecting elements using precise geometry`_ lets you query for elements that
|
||||
contain a point, a sphere, or another element. This is similar to selecting
|
||||
using bounding boxes, but additionally considers the actual geometry of the
|
||||
element. This is slower but more precise.
|
||||
- `Selecting elements using a ray`_ lets you query for elements that intersect
|
||||
with a ray.
|
||||
|
||||
Detecting intersection clashes between elements
|
||||
-----------------------------------------------
|
||||
|
||||
``clash_intersection_many`` detects when an element intersects with or is
|
||||
contained within another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_intersection_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
tolerance=0.002, # Any protrusions less than 2mm are ignored
|
||||
check_all=True, # Keep on checking all potential intersections to find a worst case protrusion distance
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# Potential clash types that can be detected are protrusions, pierces, and collisions
|
||||
clash_type = ["protrusion", "pierce", "collision", "clearance"][clash.clash_type],
|
||||
|
||||
# P1 and P2 represents two XYZ coordinates. The meaning of the coordinate depends on the clash type.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the protrusion or piercing distance in meters.
|
||||
# It is also the distance between P1 and P2.
|
||||
distance = clash.distance
|
||||
|
||||
If you specify a ``tolerance`` value, intersections with a protrusion distance
|
||||
smaller than this tolerance are excluded. It is recommended to specify a
|
||||
non-zero tolerance to distinguish between when elements merely touch (e.g. a
|
||||
GPO on a wall) versus if they are truly intersecting (e.g. a pipe going through
|
||||
a beam).
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as an
|
||||
intersection is found. This is faster but may not return the worst-case
|
||||
protrusion distance. If you are not interested in the protrusion distance, it
|
||||
is recommended to set this to ``False``. If you want the protrusion distance,
|
||||
such as to prioritise which clashes are more severe, set this to ``True``.
|
||||
|
||||
This includes:
|
||||
|
||||
1. When an element X protrudes inside element Y, where element Y is manifold.
|
||||
In this case, a protrusion distance is calculated as the deepest point of
|
||||
element X to the closest surface of element Y. ``P1`` is defined as the XYZ
|
||||
coordinate on element X, and ``P2`` is defined as the nearest point on the
|
||||
surface of element Y.
|
||||
2. When an element X pierces element Y, such that an edge of element X enters
|
||||
element Y and leaves through another face. In this case, a piercing distance
|
||||
is calculated as the distance where that edge is inside element Y. ``P1`` is
|
||||
defined as the point on an edge of element X which enters element Y, and
|
||||
``P2`` is the point where that edge leaves element Y.
|
||||
3. When neither X or Y is manifold, we cannot detect protrusion or piercing, so
|
||||
instead when X and Y have any touching face. This is the same as the
|
||||
``clash_collision_many`` check below. The distance is considered to be zero
|
||||
and ignores your specified tolerance. ``P1`` and ``P2`` are equal and
|
||||
represent an arbitrary XYZ point where the two elements touch.
|
||||
|
||||
Detecting collision clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_collision_many`` detects when the surface of an element collides with
|
||||
another element. The surfaces may either merely touch (e.g. are coplanar) or
|
||||
intersect.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_collision_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
allow_touching=True, # Include results where faces merely touch but do not intersect
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents two possible arbitrary points where a collision is found.
|
||||
# P1 may or may not be equal to P2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
A collision between two surface triangles may be "touching" or "intersecting".
|
||||
Two touching triangles may be coplanar or merely have a single edge or vertex
|
||||
touching the other triangle. An intersecting triangle will have at least one
|
||||
edge that goes through the other triangle.
|
||||
|
||||
Detecting clearance clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_clearance_many`` detects with the surface of an element comes within a
|
||||
clearance distance threshold of another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_clearance_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
clearance=0.1, # Any surface closer than than 100mm is a clash
|
||||
check_all=False, # Stop measuring distances once the first clearance violation is found per element.
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents the two XYZ coordinates between element1 and element2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the distance between element1 and element2 that is less than the clearance.
|
||||
# It is the distance between P1 and P2. It cannot be less than 0.
|
||||
distance = clash.distance
|
||||
|
||||
You cannot specify a ``clearance`` less than 0.
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as a
|
||||
clearance violation is found. This is faster but may not return the worst-case
|
||||
distance. If you only interested whether there is a clearance issue, it is
|
||||
recommended to set this to ``False``. If you want the exact worst case
|
||||
clearance distance, such as to prioritise which clashes are more severe, set
|
||||
this to ``True``.
|
||||
|
||||
Selecting elements using bounding boxes
|
||||
---------------------------------------
|
||||
|
||||
Elements may be queried using an axis aligned bounding box. An axis aligned
|
||||
bounding box is the bounding box using global XYZ axes, not the element's local
|
||||
XYZ axes. If you have a vertical construction project, this means that your
|
||||
model should be oriented to project north to get the best results.
|
||||
|
||||
You may select all elements that have a bounding box containing the point with
|
||||
XYZ coordinates of ``(0., 0., 0.)``.
|
||||
|
||||
@@ -128,11 +311,11 @@ geometry. It will return:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
elements = tree.select_box(wall, completely_within=True)
|
||||
elements = tree.select(wall, completely_within=True)
|
||||
|
||||
# Alternatively, you may also specify an extension to dilate the geometry
|
||||
# of the wall.
|
||||
elements = tree.select_box(wall, completely_within=True, extend=5.)
|
||||
elements = tree.select(wall, completely_within=True, extend=5.)
|
||||
|
||||
Selecting elements using a ray
|
||||
------------------------------
|
||||
|
||||
@@ -30,10 +30,6 @@ the API.
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| | Linux 64bit | Windows 32bit | Windows 64bit | MacOS Intel 64bit | MacOS Silicon 64bit |
|
||||
+=============+================+================+================+===================+=====================+
|
||||
| Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ |
|
||||
@@ -43,36 +39,26 @@ the API.
|
||||
| Python 3.12 | py312-linux64_ | py312-win32_ | py312-win64_ | py312-macos64_ | py312-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
|
||||
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win32.zip
|
||||
.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win32.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win32.zip
|
||||
.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win64.zip
|
||||
.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win64.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win64.zip
|
||||
.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-linux64.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-win32.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-win64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-macos64.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
|
||||
Python path. If you're not sure where your Python path is, run the following
|
||||
|
||||
@@ -61,7 +61,8 @@ operating systems. GCC (4.7 or newer) or Clang (any version) is required.
|
||||
|
||||
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
|
||||
|
||||
3. Install OpenCascade Technology (OCCT).
|
||||
3. Install OpenCascade Technology (OCCT). Officially v7.5.0 is supported. Other
|
||||
versions may have unexpected behaviour.
|
||||
|
||||
::
|
||||
|
||||
|
||||
@@ -195,6 +195,18 @@ class tree(ifcopenshell_wrapper.tree):
|
||||
args.append(kwargs.get("extend", -1.0e-5))
|
||||
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)]
|
||||
|
||||
def clash_intersection_many(self, set_a, set_b, tolerance=0.002, check_all=True):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], tolerance, check_all]
|
||||
return ifcopenshell_wrapper.tree.clash_intersection_many(*args)
|
||||
|
||||
def clash_collision_many(self, set_a, set_b, allow_touching=False):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], allow_touching]
|
||||
return ifcopenshell_wrapper.tree.clash_collision_many(*args)
|
||||
|
||||
def clash_clearance_many(self, set_a, set_b, clearance=0.05, check_all=False):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], clearance, check_all]
|
||||
return ifcopenshell_wrapper.tree.clash_clearance_many(*args)
|
||||
|
||||
|
||||
def create_shape(
|
||||
settings: settings, inst: entity_instance, repr: Optional[entity_instance] = None
|
||||
|
||||
@@ -179,12 +179,14 @@ def get_property_definition(
|
||||
if not definition:
|
||||
return
|
||||
|
||||
ifc_class = definition.is_a()
|
||||
|
||||
if prop:
|
||||
if definition.is_a("IfcElementQuantity"):
|
||||
if ifc_class == "IfcElementQuantity":
|
||||
return get_quantity(definition.Quantities, prop, verbose=verbose)
|
||||
elif definition.is_a("IfcPropertySet"):
|
||||
elif ifc_class == "IfcPropertySet":
|
||||
return get_property(definition.HasProperties, prop, verbose=verbose)
|
||||
elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"):
|
||||
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
||||
return get_property(definition.Properties, prop, verbose=verbose)
|
||||
else:
|
||||
# Entity introduced in IFC4
|
||||
@@ -196,12 +198,12 @@ def get_property_definition(
|
||||
return
|
||||
|
||||
props = {}
|
||||
if definition.is_a("IfcElementQuantity"):
|
||||
props.update(get_quantities(definition.Quantities, verbose=verbose))
|
||||
elif definition.is_a("IfcPropertySet"):
|
||||
props.update(get_properties(definition.HasProperties, verbose=verbose))
|
||||
elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"):
|
||||
props.update(get_properties(definition.Properties, verbose=verbose))
|
||||
if ifc_class == "IfcElementQuantity":
|
||||
props.update(get_quantities(definition[5], verbose=verbose))
|
||||
elif ifc_class == "IfcPropertySet":
|
||||
props.update(get_properties(definition[4], verbose=verbose))
|
||||
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
||||
props.update(get_properties(definition[2], verbose=verbose))
|
||||
else:
|
||||
# Entity introduced in IFC4
|
||||
# definition.is_a('IfcPreDefinedPropertySet'):
|
||||
@@ -216,7 +218,7 @@ def get_quantity(
|
||||
quantities: list[ifcopenshell.entity_instance], name: str, verbose=False
|
||||
) -> Union[Any, dict[str, Any]]:
|
||||
for quantity in quantities or []:
|
||||
if quantity.Name != name:
|
||||
if quantity[0] != name:
|
||||
continue
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
result = quantity[3]
|
||||
@@ -234,23 +236,23 @@ def get_quantities(quantities: list[ifcopenshell.entity_instance], verbose=False
|
||||
results = {}
|
||||
for quantity in quantities or []:
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
results[quantity.Name] = quantity[3]
|
||||
results[quantity[0]] = quantity[3]
|
||||
if verbose:
|
||||
results[quantity.Name] = {
|
||||
results[quantity[0]] = {
|
||||
"id": quantity.id(),
|
||||
"class": quantity.is_a(),
|
||||
"value": results[quantity.Name],
|
||||
"value": results[quantity[0]],
|
||||
}
|
||||
elif quantity.is_a("IfcPhysicalComplexQuantity"):
|
||||
data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"}
|
||||
data["properties"] = get_quantities(quantity.HasQuantities)
|
||||
del data["HasQuantities"]
|
||||
results[quantity.Name] = data
|
||||
results[quantity[0]] = data
|
||||
if verbose:
|
||||
results[quantity.Name] = {
|
||||
results[quantity[0]] = {
|
||||
"id": quantity.id(),
|
||||
"class": quantity.is_a(),
|
||||
"value": results[quantity.Name],
|
||||
"value": results[quantity[0]],
|
||||
}
|
||||
return results
|
||||
|
||||
@@ -262,7 +264,7 @@ def get_property(
|
||||
if prop.Name != name:
|
||||
continue
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
result = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
||||
result = prop[2].wrappedValue if prop[2] else None
|
||||
elif prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
result = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
elif prop.is_a("IfcPropertyListValue"):
|
||||
@@ -286,35 +288,36 @@ def get_property(
|
||||
def get_properties(properties: list[ifcopenshell.entity_instance], verbose=False) -> dict[str, dict[str, Any]]:
|
||||
results = {}
|
||||
for prop in properties or []:
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
results[prop.Name] = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
||||
ifc_class = prop.is_a()
|
||||
if ifc_class == "IfcPropertySingleValue":
|
||||
results[prop[0]] = prop[2].wrappedValue if prop[2] else None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
results[prop.Name] = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyEnumeratedValue":
|
||||
results[prop[0]] = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyListValue"):
|
||||
results[prop.Name] = [v.wrappedValue for v in prop.ListValues] or None
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyListValue":
|
||||
results[prop[0]] = [v.wrappedValue for v in prop.ListValues] or None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyBoundedValue"):
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyBoundedValue":
|
||||
data = prop.get_info()
|
||||
del data["Unit"]
|
||||
results[prop.Name] = data
|
||||
results[prop[0]] = data
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyTableValue"):
|
||||
results[prop.Name] = prop.get_info()
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyTableValue":
|
||||
results[prop[0]] = prop.get_info()
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcComplexProperty"):
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcComplexProperty":
|
||||
data = {k: v for k, v in prop.get_info().items() if v is not None and k != "Name"}
|
||||
data["properties"] = get_properties(prop.HasProperties)
|
||||
del data["HasProperties"]
|
||||
results[prop.Name] = data
|
||||
results[prop[0]] = data
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import pathlib
|
||||
import ifcopenshell
|
||||
@@ -25,10 +26,10 @@ from ifcopenshell.entity_instance import entity_instance
|
||||
from functools import lru_cache
|
||||
from typing import List, Generator, Optional
|
||||
|
||||
templates = {}
|
||||
templates: dict[str, PsetQto] = {}
|
||||
|
||||
|
||||
def get_template(schema):
|
||||
def get_template(schema: str) -> PsetQto:
|
||||
global templates
|
||||
if schema not in templates:
|
||||
templates[schema] = PsetQto(schema)
|
||||
@@ -36,11 +37,13 @@ def get_template(schema):
|
||||
|
||||
|
||||
class PsetQto:
|
||||
# fmt: off
|
||||
templates_path = {
|
||||
"IFC2X3": "Pset_IFC2X3.ifc",
|
||||
"IFC4": "Pset_IFC4_ADD2.ifc",
|
||||
"IFC4X3": "Pset_IFC4X3.ifc"
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, schema: str, templates=None) -> None:
|
||||
self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from fractions import Fraction
|
||||
from math import pi
|
||||
from typing import Tuple, Iterable, Any, Union, Literal
|
||||
from typing import Tuple, Iterable, Any, Union, Literal, Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
@@ -525,7 +525,7 @@ def convert_unit(value, from_unit, to_unit):
|
||||
)
|
||||
|
||||
|
||||
def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
def convert(value: float, from_prefix: Optional[str], from_unit: str, to_prefix: Optional[str], to_unit: str) -> float:
|
||||
"""Converts between length, area, and volume units
|
||||
|
||||
In this case, you manually specify the names and (optionally) prefixes to
|
||||
@@ -534,12 +534,12 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
|
||||
:param value: The numeric value you want to convert
|
||||
:type value: float
|
||||
:param from_prefix: A prefix from IfcSIPrefix. Can be None.
|
||||
:type from_prefix: str,optional
|
||||
:param from_prefix: A prefix from IfcSIPrefix. Can be None
|
||||
:type from_prefix: str, optional
|
||||
:param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
|
||||
:type from_unit: str
|
||||
:param to_prefix: A prefix from IfcSIPrefix. Can be None.
|
||||
:type to_prefix: str,optional
|
||||
:param to_prefix: A prefix from IfcSIPrefix. Can be None
|
||||
:type to_prefix: str, optional
|
||||
:param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
|
||||
:type to_unit: str
|
||||
:return: The converted value.
|
||||
@@ -566,7 +566,7 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
return value
|
||||
|
||||
|
||||
def calculate_unit_scale(ifc_file, unit_type="LENGTHUNIT"):
|
||||
def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUNIT") -> float:
|
||||
"""Returns a unit scale factor to convert to and from IFC project units and SI units.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -16,7 +16,7 @@ classifiers = [
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
geometry = ["mathutils", "shapely"]
|
||||
date = ["isodate"]
|
||||
date = ["isodate", "dateutil"]
|
||||
[project.urls]
|
||||
"Homepage" = "http://ifcopenshell.org"
|
||||
"Bug Tracker" = "https://github.com/ifcopenshell/ifcopenshell/issues"
|
||||
|
||||
@@ -740,6 +740,8 @@ size_t IfcParse::IfcFile::load(unsigned entity_instance_name, const IfcParse::en
|
||||
filler.push_back(ea);
|
||||
} catch (IfcException& e) {
|
||||
Logger::Message(Logger::LOG_ERROR, e.what());
|
||||
// #4070 We didn't actually capture an aggregate entry, undo length increment.
|
||||
return_value--;
|
||||
}
|
||||
} else {
|
||||
filler.push_back(new TokenArgument(next));
|
||||
|
||||
@@ -52,8 +52,8 @@ class Patcher:
|
||||
self.query = query
|
||||
|
||||
def patch(self):
|
||||
self.contained_ins = {}
|
||||
self.aggregates = {}
|
||||
self.contained_ins: dict[str, set[ifcopenshell.entity_instance]] = {}
|
||||
self.aggregates: dict[str, set[ifcopenshell.entity_instance]] = {}
|
||||
self.new = ifcopenshell.file(schema=self.file.wrapped_data.schema)
|
||||
self.owner_history = None
|
||||
self.reuse_identities: dict[int, ifcopenshell.entity_instance] = {}
|
||||
@@ -66,18 +66,14 @@ class Patcher:
|
||||
self.create_spatial_tree()
|
||||
self.file = self.new
|
||||
|
||||
def add_element(self, element) -> None:
|
||||
def add_element(self, element: ifcopenshell.entity_instance) -> None:
|
||||
new_element = self.append_asset(element)
|
||||
if not new_element:
|
||||
return
|
||||
for rel in getattr(element, "ContainedInStructure", []):
|
||||
spatial_element = rel.RelatingStructure
|
||||
new_spatial_element = self.append_asset(spatial_element)
|
||||
self.contained_ins.setdefault(spatial_element.GlobalId, set()).add(new_element)
|
||||
self.add_decomposition_parents(spatial_element, new_spatial_element)
|
||||
self.add_spatial_structures(element, new_element)
|
||||
self.add_decomposition_parents(element, new_element)
|
||||
|
||||
def append_asset(self, element) -> Union[ifcopenshell.entity_instance, None]:
|
||||
def append_asset(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
try:
|
||||
return self.new.by_guid(element.GlobalId)
|
||||
except:
|
||||
@@ -88,29 +84,43 @@ class Patcher:
|
||||
"project.append_asset", self.new, library=self.file, element=element, reuse_identities=self.reuse_identities
|
||||
)
|
||||
|
||||
def add_decomposition_parents(self, element, new_element) -> None:
|
||||
def add_spatial_structures(
|
||||
self, element: ifcopenshell.entity_instance, new_element: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
"""element is IfcElement"""
|
||||
for rel in getattr(element, "ContainedInStructure", []):
|
||||
spatial_element = rel.RelatingStructure
|
||||
new_spatial_element = self.append_asset(spatial_element)
|
||||
self.contained_ins.setdefault(spatial_element.GlobalId, set()).add(new_element)
|
||||
self.add_decomposition_parents(spatial_element, new_spatial_element)
|
||||
|
||||
def add_decomposition_parents(
|
||||
self, element: ifcopenshell.entity_instance, new_element: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
"""element is IfcObjectDefinition"""
|
||||
for rel in element.Decomposes:
|
||||
parent = rel.RelatingObject
|
||||
new_parent = self.append_asset(parent)
|
||||
self.aggregates.setdefault(parent.GlobalId, set()).add(new_element)
|
||||
self.add_decomposition_parents(parent, new_parent)
|
||||
self.add_spatial_structures(parent, new_parent)
|
||||
|
||||
def create_spatial_tree(self) -> None:
|
||||
for relating_structure, related_elements in self.contained_ins.items():
|
||||
for relating_structure_guid, related_elements in self.contained_ins.items():
|
||||
self.new.createIfcRelContainedInSpatialStructure(
|
||||
ifcopenshell.guid.new(),
|
||||
self.owner_history,
|
||||
None,
|
||||
None,
|
||||
list(related_elements),
|
||||
self.new.by_guid(relating_structure),
|
||||
self.new.by_guid(relating_structure_guid),
|
||||
)
|
||||
for relating_object, related_objects in self.aggregates.items():
|
||||
for relating_object_guid, related_objects in self.aggregates.items():
|
||||
self.new.createIfcRelAggregates(
|
||||
ifcopenshell.guid.new(),
|
||||
self.owner_history,
|
||||
None,
|
||||
None,
|
||||
self.new.by_guid(relating_object),
|
||||
self.new.by_guid(relating_object_guid),
|
||||
list(related_objects),
|
||||
)
|
||||
|
||||
@@ -20,11 +20,59 @@ import os
|
||||
import pytest
|
||||
import ifcpatch
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
class TestExtractElements:
|
||||
def test_basic(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
|
||||
wall = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
|
||||
output = ifcpatch.execute({"file": ifc_file, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
|
||||
assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId
|
||||
assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId
|
||||
|
||||
def test_keep_spatial_structure(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
|
||||
|
||||
site = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcSite")
|
||||
building = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcBuilding")
|
||||
storey = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcBuildingStorey")
|
||||
wall = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.run("aggregate.assign_object", ifc_file, product=building, relating_object=site)
|
||||
ifcopenshell.api.run("aggregate.assign_object", ifc_file, product=storey, relating_object=building)
|
||||
ifcopenshell.api.run("spatial.assign_container", ifc_file, product=wall, relating_structure=storey)
|
||||
|
||||
output = ifcpatch.execute({"file": ifc_file, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
|
||||
wall_new = output.by_type("IfcWall")[0]
|
||||
assert (storey_new := ifcopenshell.util.element.get_container(wall_new)).GlobalId == storey.GlobalId
|
||||
assert (building_new := ifcopenshell.util.element.get_aggregate(storey_new)).GlobalId == building.GlobalId
|
||||
assert (site_new := ifcopenshell.util.element.get_aggregate(building_new)).GlobalId == site.GlobalId
|
||||
|
||||
def test_keep_aggregate_in_spatial_structure(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
|
||||
|
||||
element = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcElementAssembly")
|
||||
container = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcBuildingStorey")
|
||||
subelement = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.run("spatial.assign_container", ifc_file, product=element, relating_structure=container)
|
||||
ifcopenshell.api.run("aggregate.assign_object", ifc_file, product=subelement, relating_object=element)
|
||||
|
||||
output = ifcpatch.execute({"file": ifc_file, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
|
||||
wall_new = output.by_type("IfcWall")[0]
|
||||
assembly = output.by_type("IfcElementAssembly")[0]
|
||||
|
||||
assert ifcopenshell.util.element.get_aggregate(wall_new).GlobalId == element.GlobalId
|
||||
assert ifcopenshell.util.element.get_container(assembly).GlobalId == container.GlobalId
|
||||
|
||||
def test_getting_the_psets_of_a_product_as_a_dictionary(self):
|
||||
ifc = ifcopenshell.open(os.path.join(os.getcwd(), "test", "files", "basic.ifc"))
|
||||
output = ifcpatch.execute({"input": ifc, "recipe": "ExtractElements", "arguments": [".IfcWall"]})
|
||||
output = ifcpatch.execute({"file": ifc, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
assert output.by_type("IfcWall")
|
||||
assert not output.by_type("IfcSlab")
|
||||
|
||||
@@ -82,6 +82,8 @@ std::pair<char const*, size_t> vector_to_buffer(const T& t) {
|
||||
|
||||
%template(ray_intersection_results) std::vector<IfcGeom::ray_intersection_result>;
|
||||
|
||||
%template(clashes) std::vector<IfcGeom::clash>;
|
||||
|
||||
// A Template instantantation should be defined before it is used as a base class.
|
||||
// But frankly I don't care as most methods are subtlely different anyway.
|
||||
%include "../ifcgeom_schema_agnostic/IfcGeomTree.h"
|
||||
@@ -114,6 +116,80 @@ std::pair<char const*, size_t> vector_to_buffer(const T& t) {
|
||||
return IfcGeom_tree_vector_to_list(ps);
|
||||
}
|
||||
|
||||
|
||||
%typemap(in) const std::vector<IfcUtil::IfcBaseClass*>& (std::vector<IfcUtil::IfcBaseClass*> temp) {
|
||||
if (!PyList_Check($input)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Expected a list.");
|
||||
return NULL;
|
||||
}
|
||||
$1 = &temp; // Set $1 to the address of temp, which SWIG will use as the argument in the wrapped function
|
||||
temp.reserve(PyList_Size($input)); // Pre-allocate memory for efficiency
|
||||
for (Py_ssize_t i = 0; i < PyList_Size($input); ++i) {
|
||||
PyObject* pyObj = PyList_GetItem($input, i);
|
||||
void* ptr = 0;
|
||||
int res = SWIG_ConvertPtr(pyObj, &ptr, SWIGTYPE_p_IfcUtil__IfcBaseClass, 0);
|
||||
if (!SWIG_IsOK(res)) {
|
||||
PyErr_SetString(PyExc_TypeError, "List item is not of type IfcBaseClass.");
|
||||
return NULL;
|
||||
}
|
||||
temp.push_back(reinterpret_cast<IfcUtil::IfcBaseClass*>(ptr));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<clash> clash_intersection_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, double tolerance, bool check_all) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_intersection_many(set_a_entities, set_b_entities, tolerance, check_all);
|
||||
}
|
||||
|
||||
std::vector<clash> clash_collision_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, bool allow_touching) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_collision_many(set_a_entities, set_b_entities, allow_touching);
|
||||
}
|
||||
|
||||
std::vector<clash> clash_clearance_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, double clearance, bool check_all) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_clearance_many(set_a_entities, set_b_entities, clearance, check_all);
|
||||
}
|
||||
|
||||
aggregate_of_instance::ptr select(IfcUtil::IfcBaseClass* e, bool completely_within = false, double extend = 0.0) const {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("Instance should be an IfcProduct");
|
||||
|
||||
@@ -203,7 +203,14 @@
|
||||
|
||||
%include "IfcGeomWrapper.i"
|
||||
%include "IfcParseWrapper.i"
|
||||
%include "std_vector.i"
|
||||
|
||||
namespace std {
|
||||
%template(float_array_3) array<double, 3>;
|
||||
%template(FloatVector) vector<float>;
|
||||
%template(IntVector) std::vector<int>;
|
||||
%template(DoubleVector) std::vector<double>;
|
||||
%template(StringVector) std::vector<std::string>;
|
||||
%template(FloatVectorVector) std::vector<std::vector<float>>;
|
||||
%template(DoubleVectorVector) std::vector<std::vector<double>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user