Compare commits

...

43 Commits

Author SHA1 Message Date
Dion Moult 413886ac6b Fix #4411. Fallback to absolute paths if relative paths fail. 2024-03-29 15:29:19 +11:00
Dion Moult 377f68c020 Run black on IfcTester 2024-03-29 15:28:33 +11:00
Dion Moult b718a37db2 Fix #4427. Failures are now stored in single dictionary.
There was a missing clear() call to failed_reasons which could lead to incorrect reporting. It also makes no sense to store them separately.
2024-03-29 15:06:59 +11:00
Dion Moult e21043b038 Fix #4465. Update IfcTester to IDS 0.9.7 2024-03-28 22:47:48 +11:00
Andrej730 395598d8e3 Fix invalid profiles after subdividing edges near arcs #4471 2024-03-28 14:02:03 +05:00
Andrej730 813518bcb5 use blender units subtype for cad props 2024-03-28 14:02:03 +05:00
Andrej730 4ce7a898c9 typing, format 2024-03-28 14:02:02 +05:00
Thomas Krijnen 78f235ea7c #4273 ignore degenerate triangles in mesher output 2024-03-28 09:06:41 +01:00
Vukas Pajic bda2f50f9d sync with latest 2024-03-28 17:15:58 +11:00
Vukas Pajic 5e5ba5b3d4 Additional metadata as property set #4425
https://github.com/IfcOpenShell/IfcOpenShell/issues/4425
2024-03-28 17:15:58 +11:00
Dion Moult 22b0e9dcc5 Minor doc fixes to refer to Mac intel and silicon 2024-03-28 17:13:50 +11:00
Dion Moult 68420d43ea Clash results now are sorted and show text overlays of clash type and distance 2024-03-28 17:11:46 +11:00
Dion Moult 924e337c90 Don't consider borderline points to be inside a shape for more robust protrusion results 2024-03-28 16:57:54 +11:00
Dion Moult 04599a10d2 Change precision check of "is point on line" to be 0.01mm
Precision::Confusion() is 1e-7 which is too small apparently when doing this math. There are plenty of results which are things like 5e-7.
2024-03-28 16:57:54 +11:00
Dion Moult 84c931bb5f Fix bug where hidden elements weren't selected sometimes 2024-03-28 16:52:30 +11:00
Dion Moult 572aedb3eb Fix #4269. Include dateutil for ifcopenshell on pypi 2024-03-28 10:48:19 +11:00
Dion Moult 1db86c24e3 Bump IfcOpenShell and remove py37 py38 from installation docs 2024-03-28 10:47:44 +11:00
Andrej730 74adde3030 ExtractElemenets - add some tests 2024-03-27 17:52:18 +05:00
Andrej730 f68e8f0e00 ExtractElements - fix bug where IfcElementAssembly elements wasn't added to the spatial tree
Example issue - https://community.osarch.org/discussion/2054/bbim-extractelements-patch-and-missing-elements-in-ifcelementassembly
2024-03-27 16:59:10 +05:00
Andrej730 98386984f3 Fix tests involving changes in Blender 4.1 (will fail on Blender <4.1)
Basically in Blender 4.1 they've changed a way renaming IDs works if name already existed before and those tests will stop working on Blender <4.1, see https://projects.blender.org/blender/blender/issues/119955
2024-03-27 15:23:46 +05:00
Andrej730 7b35ed97ca tests - reduce precision to our usual Cad tool precision to fix some tests 2024-03-27 15:07:49 +05:00
Andrej730 b3302ed679 Fix test in 1c1035e
tag @brunoperdigao for information
2024-03-27 15:07:49 +05:00
Andrej730 e12f060af8 black format 2024-03-27 15:07:49 +05:00
Dion Moult 6d4f47a2ca New entourage library including people and cars 2024-03-27 20:23:44 +11:00
Thomas Krijnen e49ab7e0cc #4070 don't increment aggregate length on invalid members 2024-03-27 09:36:05 +01:00
Dion Moult ccfe7ab80a Optimise property / quantity checks for IDS and other purposes 2024-03-27 16:09:13 +11:00
Dion Moult 7b95358975 Forgot to commit clash tool 2024-03-27 16:06:47 +11:00
Dion Moult bcc50aaadc Switch over clash to latest add_element signature 2024-03-27 16:06:15 +11:00
Dion Moult 30f63b7d67 Minor note about which OCCT version is currently supported 2024-03-27 15:59:25 +11:00
Dion Moult ead13e23bc Write docs about new geometry tree clash functions 2024-03-27 15:59:25 +11:00
Thomas Krijnen 24257d2a85 checkout v0.7.0 -- src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp 2024-03-26 15:13:36 +01:00
Dion Moult 5c4dbb924d Reinclude missing max_protrusions_ 2024-03-26 15:13:36 +01:00
Thomas Krijnen 88e1b3d2f9 Fix is_manifold(), sorry :( 2024-03-26 15:13:36 +01:00
Thomas Krijnen f67b80c6a6 Integrate @Moult's add_element(TriangulationElement*) 2024-03-26 15:13:36 +01:00
Dion Moult c7fd42f2cf Clash detection features 2024-03-26 15:13:36 +01:00
Thomas Krijnen bed03bf83c #4307 2024-03-26 12:27:05 +01:00
Andrej730 1e49400e93 fix styles tests 2024-03-26 16:12:13 +05:00
Andrej730 608c0dc05d Fix tests, clean up file creating empty project
To fix tests changed bim.unlink_object not to unlink original blender materials as they might be used elsewhere.
2024-03-26 15:59:23 +05:00
Andrej730 921dbe9dae restore accidentally unregistered operator in 9be7d95 (caused issue in tests) 2024-03-26 15:54:46 +05:00
Andrej730 a7e0837f96 small optimization
not to calculate each quantity twice
2024-03-26 15:54:46 +05:00
Andrej730 071c80f845 fix tests 2024-03-26 15:54:46 +05:00
Andrej730 06d823b9d8 typing 2024-03-26 15:54:45 +05:00
Andrej730 b15e33e5e9 fix test after 7634e71 2024-03-26 14:32:16 +05:00
65 changed files with 33345 additions and 769 deletions
+1 -1
View File
@@ -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
@@ -24,10 +24,10 @@ from math import pi
class BIMCadProperties(PropertyGroup):
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
x: bpy.props.FloatProperty(name="X", default=0.2)
y: bpy.props.FloatProperty(name="Y", default=0.1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
@@ -20,6 +20,7 @@ import os
import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
@@ -239,8 +240,9 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -248,13 +250,15 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
def hotkey_S_O(self):
bpy.ops.bim.cad_offset(distance=self.props.distance)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
def hotkey_S_Q(self):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -270,7 +274,8 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_R(self):
if self.is_profile():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
@@ -16,31 +16,37 @@
# 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 blf
import gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
class ClashDecorator:
installed = None
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.installed:
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
@@ -48,7 +54,27 @@ class ClashDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
def draw_text(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
color = self.addon_prefs.decorations_colour
blf.color(font_id, *color)
if coords_2d:
w, h = blf.dimensions(font_id, text)
coords_2d -= Vector((w * .5, 0))
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
blf.draw(font_id, text) # Set your text here
def draw_geometry(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
@@ -69,6 +69,7 @@ class ImportClashSets(bpy.types.Operator):
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = clash_set["name"]
@@ -276,14 +277,7 @@ class ExecuteIfcClash(bpy.types.Operator):
if extension == ".json":
tool.Clash.load_clash_sets(self.filepath)
result = tool.Clash.get_clash_set(self.props.active_clash_set.name)
for clash in result["clashes"].values():
blender_clash = self.props.active_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"]
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -379,6 +373,7 @@ class SelectClash(bpy.types.Operator):
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
self.props.p1 = clash["p1"]
self.props.p2 = clash["p2"]
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
return {"FINISHED"}
@@ -97,6 +97,7 @@ class BIMClashProperties(PropertyGroup):
)
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
active_clash_text: StringProperty(name="Active Clash Text")
@property
def active_clash_set(self):
@@ -21,12 +21,14 @@ import bmesh
import mathutils
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.tool as tool
from math import pi, pow
from mathutils import Vector, Matrix, geometry
from typing import Union
class Helper:
def __init__(self, file):
def __init__(self, file: ifcopenshell.file):
self.file = file
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -34,7 +36,7 @@ class Helper:
# edge that shares a single vertex only with that face to find the extrusion
# edge. A face with the normal facing down is prioritised. A limited
# dissolve ensure that faces are quads and not tris.
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
@@ -115,7 +117,9 @@ class Helper:
return {"profile": profile, "extrusion": extrusion}
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
def auto_detect_arbitrary_profile_with_voids(
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
) -> Union[tuple, dict]:
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
@@ -138,28 +142,29 @@ class Helper:
total_groups = 0
is_circle = False
for group_type, group_indices in groups.items():
for group_index in group_indices:
if group_index in vert[deform_layer]:
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
if not is_special:
continue
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if total_groups > 1: # A vert can only belong to one group
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
elif is_circle:
pass # Circles are allowed to be unclosed
pass # Circles are allowed to be unclosed
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
for group_type, group_counts in group_verts.items():
if group_type == "IFCARCINDEX":
for group_count in group_counts.values():
if group_count != 3: # Each arc needs 3 verts
if group_count != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
elif group_type == "IFCCIRCLE":
for group_count in group_counts.values():
if group_count != 2: # Each circle needs 2 verts
if group_count != 2: # Each circle needs 2 verts
return (False, "CIRCLE")
loop_edges = set(bm.edges)
@@ -269,7 +274,7 @@ class Helper:
inner_loops.remove(outer_loop)
# Copy vectors to prevent random data mangling after bmesh is freed.
points = [Vector(list(v.co)) for v in bm.verts]
points = [v.co.copy() for v in bm.verts]
bm.to_mesh(mesh)
mesh.update()
@@ -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):
@@ -23,6 +23,7 @@ from math import sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from typing import Union
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
@@ -35,15 +36,6 @@ def transparent_color(color, alpha=0.1):
return color
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
"""returns tuple boolean (whether vertex is in any of the groups)
and related group index"""
for group_index in vertex[deform_layer].keys():
if group_index in groups:
return True, group_index
return False, None
class ProfileDecorator:
installed = None
@@ -148,12 +140,12 @@ class ProfileDecorator:
# deform_layer is None if there are no verts assigned to vertex groups
# even if there are vertex groups in the obj.vertex_groups
if deform_layer:
is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups)
is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups)
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups)
is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups)
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
@@ -20,9 +20,12 @@ import bpy
import json
import bmesh
import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import ifcopenshell.util.type
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.geometry
@@ -698,9 +701,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
)
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
@@ -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()
@@ -856,7 +860,10 @@ class LinkIfc(bpy.types.Operator):
continue
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
try:
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
except:
pass # Perhaps on another drive or something
new.name = filepath
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
print(f"Finished linking {len(files)} IFCs", time.time() - start)
@@ -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
+12 -1
View File
@@ -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
@@ -508,7 +519,7 @@ class Misc:
class Model:
def convert_si_to_unit(cls, value): pass
def convert_unit_to_si(cls, value): pass
def export_curve(cls, position, edge_indices, points=None): pass
def export_curve(cls, position, edge_indices): pass
def export_points(cls, position, indices): pass
def export_profile(cls, obj, position=None): pass
def generate_occurrence_name(cls, element_type, ifc_class): 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
+12
View File
@@ -569,6 +569,18 @@ class Blender(blenderbim.core.tool.Blender):
return bm_a
@classmethod
def bmesh_check_vertex_in_groups(
cls, vertex: bmesh.types.BMVert, deform_layer: bmesh.types.BMLayerItem, groups: list[int]
) -> Union[tuple[Literal[True], int], tuple[Literal[False], None]]:
"""returns tuple boolean (whether vertex is in any of the groups) and related group index"""
for group_index in vertex[deform_layer].keys():
# ignore vertex groups assignments produced by edge subdivision near arcs
# they usually have weight = 0.5
if group_index in groups and vertex[deform_layer][group_index] == 1.0:
return True, group_index
return False, None
@classmethod
def toggle_edit_mode(cls, context: bpy.types.Context) -> set:
ao = context.active_object
+116
View File
@@ -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 sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]):
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
+1 -1
View File
@@ -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:
+27 -18
View File
@@ -20,6 +20,7 @@ import bpy
import json
import bmesh
import collections
import collections.abc
import numpy as np
import ifcopenshell
import ifcopenshell.util.unit
@@ -36,23 +37,26 @@ from blenderbim.bim import import_ifc
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
from ifcopenshell.util.shape_builder import V, ShapeBuilder
from typing import Optional, Union, TypeVar, Any
T = TypeVar("T")
class Model(blenderbim.core.tool.Model):
@classmethod
def convert_si_to_unit(cls, value):
def convert_si_to_unit(cls, value: T) -> T:
if isinstance(value, (tuple, list)):
return [v / cls.unit_scale for v in value]
return value / cls.unit_scale
@classmethod
def convert_unit_to_si(cls, value):
def convert_unit_to_si(cls, value: T) -> T:
if isinstance(value, (tuple, list)):
return [v * cls.unit_scale for v in value]
return value * cls.unit_scale
@classmethod
def convert_data_to_project_units(cls, data, non_si_props=[]):
def convert_data_to_project_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for prop_name in data:
if prop_name in non_si_props:
@@ -65,7 +69,7 @@ class Model(blenderbim.core.tool.Model):
return data
@classmethod
def convert_data_to_si_units(cls, data, non_si_props=[]):
def convert_data_to_si_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for prop_name in data:
if prop_name in non_si_props:
@@ -78,34 +82,35 @@ class Model(blenderbim.core.tool.Model):
return data
@classmethod
def export_curve(cls, position, edge_indices, points=None):
def export_curve(cls, position: Matrix, edge_indices: list[tuple[int, int]]) -> ifcopenshell.entity_instance:
position_i = position.inverted()
ifc_file = tool.Ifc.get()
if len(edge_indices) == 2:
diameter = edge_indices[0]
p1 = cls.bm.verts[diameter[0]].co
p2 = cls.bm.verts[diameter[1]].co
center = cls.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
radius = cls.convert_si_to_unit((p1 - p2).length / 2)
return tool.Ifc.get().createIfcCircle(
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius
return ifc_file.createIfcCircle(
ifc_file.createIfcAxis2Placement2D(ifc_file.createIfcCartesianPoint(center[0:2])), radius
)
if tool.Ifc.get().schema == "IFC2X3":
if ifc_file.schema == "IFC2X3":
points = []
for edge in edge_indices:
local_point = (position_i @ Vector(cls.bm.verts[edge[0]].co)).to_2d()
points.append(tool.Ifc.get().createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
points.append(ifc_file.createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
points.append(points[0])
return tool.Ifc.get().createIfcPolyline(points)
return ifc_file.createIfcPolyline(points)
segments = []
for segment in edge_indices:
if len(segment) == 2:
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
segments.append(ifc_file.createIfcLineIndex([i + 1 for i in segment]))
elif len(segment) == 3:
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
return tool.Ifc.get().createIfcIndexedPolyCurve(cls.points, segments, False)
segments.append(ifc_file.createIfcArcIndex([i + 1 for i in segment]))
return ifc_file.createIfcIndexedPolyCurve(cls.points, segments, False)
@classmethod
def export_points(cls, position, indices):
def export_points(cls, position: Matrix, indices: list[Vector]) -> ifcopenshell.entity_instance:
position_i = position.inverted()
points = []
for point in indices:
@@ -114,7 +119,10 @@ class Model(blenderbim.core.tool.Model):
return tool.Ifc.get().createIfcCartesianPointList2D(points)
@classmethod
def export_profile(cls, obj, position=None):
def export_profile(
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
) -> Union[ifcopenshell.entity_instance, None]:
"""Returns `None` in case if profile was invalid."""
if position is None:
position = Matrix()
@@ -150,7 +158,7 @@ class Model(blenderbim.core.tool.Model):
return profile
@classmethod
def export_surface(cls, obj):
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
edge1 = p2 - p1
@@ -202,7 +210,7 @@ class Model(blenderbim.core.tool.Model):
return surface
@classmethod
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str):
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str) -> str:
props = bpy.context.scene.BIMModelProperties
if props.occurrence_name_style == "CLASS":
return ifc_class[3:]
@@ -216,7 +224,8 @@ class Model(blenderbim.core.tool.Model):
return "Instance"
@classmethod
def get_extrusion(cls, representation):
def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""return first found IfcExtrudedAreaSolid"""
item = representation.Items[0]
while True:
if item.is_a("IfcExtrudedAreaSolid"):
+35 -20
View File
@@ -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
+2
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -22,8 +22,8 @@ You will need to choose which build to download.
- If you are on Blender >=4.1, choose py311
- If you are on Blender >=3.1 and <=4.0, choose py10
- If you are on Blender >=2.93 and <3.1, choose py39
- Choose ``linux``, ``macos``, ``macosm1`` (for Apple M1 devices), or ``win``
depending on your operating system
- Choose ``linux``, ``macos`` (Apple Intel), ``macosm1`` (Apple Silicon), or
``win`` depending on your operating system
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
but it happens.
+2 -2
View File
@@ -6,8 +6,8 @@ the Python version shipped by the Blender Foundation for the most recent three
major Blender versions:
- 64-bit Linux on Python 3.10
- 64-bit MacOS on Python 3.10
- 64-bit MacOS M1 on Python 3.10
- 64-bit MacOS Intel on Python 3.10
- 64-bit MacOS Silicon on Python 3.10
- 64-bit Windows on Python 3.10
Note that developer builds may exist for older versions of Python but there will
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
+42 -42
View File
@@ -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
+5 -6
View File
@@ -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
+1 -1
View File
@@ -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
+23 -16
View File
@@ -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
+1 -1
View File
@@ -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):
+29 -2
View File
@@ -18,6 +18,7 @@
import datetime
from datetime import timedelta, date
from typing import List
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.date
@@ -25,7 +26,7 @@ import xml.etree.ElementTree as ET
class MSP2Ifc:
def __init__(self):
def __init__(self, optionalColumns: List[str] = []):
self.xml = None
self.file = None
self.ns = None
@@ -33,6 +34,7 @@ class MSP2Ifc:
self.project = {}
self.calendars = {}
self.tasks = {}
self.optionalColumns = optionalColumns
self.resources = {}
self.RESOURCE_TYPES_MAPPING = {"1": "LABOR", "0": "MATERIAL", "2": None}
@@ -106,6 +108,18 @@ class MSP2Ifc:
"ifc": None,
}
# retrieve optional columns
# If first column = "all" then retrieve all columns
if len(self.optionalColumns) and self.optionalColumns[0] == "all":
self.optionalColumns = [child.tag.split("}")[1] for child in task]
for column in self.optionalColumns:
if not self.tasks[task_id].get(column):
self.tasks[task_id][column] = task.find(f"pr:{column}", self.ns).text if task.find(f"pr:{column}", self.ns) else None
def parse_calendar_xml(self, project):
def parse_working_times(day):
working_times = []
@@ -200,7 +214,8 @@ class MSP2Ifc:
def create_tasks(self, work_schedule):
for task_id in self.tasks:
task = self.tasks[task_id]
if task["OutlineLevel"] == 0:
# Outline Level can be None or 0
if not task["OutlineLevel"]:
self.create_task(task, work_schedule=work_schedule)
def create_work_schedule(self):
@@ -268,6 +283,18 @@ class MSP2Ifc:
for subtask_id in task["subtasks"]:
self.create_task(self.tasks[subtask_id], parent_task=task)
# create pset for optional columns
if len(self.optionalColumns):
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=task["ifc"] , name="Pset_MSP_Task")
ifcopenshell.api.run(
"pset.edit_pset",
self.file,
pset=pset,
properties= {name: str(task[name]) for name in self.optionalColumns if task[name]}
)
def process_working_week(self, week, calendar):
day_map = {
"1": 7, # Sunday
+1 -2
View File
@@ -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
+13 -1
View File
@@ -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;
@@ -371,6 +371,11 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
triangles(i).Get(n3, n2, n1);
else triangles(i).Get(n1, n2, n3);
if (dict[n1] == dict[n2] || dict[n2] == dict[n3] || dict[n3] == dict[n1]) {
Logger::Warning("Mesher generated a degenerate triangle, ignoring");
continue;
}
/* An alternative would be to calculate normals based
* on the coordinates of the mesh vertices */
/*
File diff suppressed because it is too large Load Diff
+588
View File
@@ -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;
}
+29
View File
@@ -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);
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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"
+2
View File
@@ -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),
)
+49 -1
View File
@@ -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")
+3 -4
View File
@@ -16,14 +16,13 @@ my_ids = ids.Ids(title="My IDS")
my_spec = ids.Specification(name="My first specification")
my_spec.applicability.append(ids.Entity(name="IFCWALL"))
property = ids.Property(
name="IsExternal",
baseName="IsExternal",
value="TRUE",
propertySet="Pset_WallCommon",
datatype="IfcBoolean",
dataType="IfcBoolean",
uri="https://identifier.buildingsmart.org/uri/.../prop/LoadBearing",
instructions="Walls need to be load bearing.",
minOccurs=1,
maxOccurs="unbounded")
cardinality="required")
my_spec.requirements.append(property)
my_ids.specifications.append(my_spec)
+69 -93
View File
@@ -56,8 +56,7 @@ def get_psets(element):
class Facet:
def __init__(self, *parameters):
self.status = None
self.failed_entities: List[Facet] = []
self.failed_reasons: List[str] = []
self.failures = []
for i, name in enumerate(self.parameters):
setattr(self, name.replace("@", ""), parameters[i])
@@ -66,22 +65,22 @@ class Facet:
for name in self.parameters:
value = getattr(self, name.replace("@", ""))
if value is not None:
if name == "@dataType":
value = value.upper()
results[name] = value if "@" in name else self.to_ids_value(value)
if clause_type == "applicability":
for key in ["@uri", "@instructions", "@minOccurs", "@maxOccurs"]:
for key in ["@uri", "@instructions", "@cardinality"]:
results.pop(key, None)
return results
def parse(self, xml):
setattr(self, "minOccurs", 1)
setattr(self, "maxOccurs", 1)
setattr(self, "cardinality", "required")
for name, value in xml.items():
name = name.replace("@", "")
if isinstance(value, dict) and "simpleValue" in value.keys():
setattr(self, name, value["simpleValue"])
elif isinstance(value, dict) and "restriction" in value.keys():
setattr(self, name, Restriction().parse(value["restriction"][0]))
# TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]]
else:
setattr(self, name, value)
return self
@@ -98,7 +97,7 @@ class Facet:
is_prohibited = False
if specification.maxOccurs == 0:
is_prohibited = not is_prohibited
if requirement.maxOccurs == 0:
if requirement.cardinality == "prohibited":
is_prohibited = not is_prohibited
templates = self.prohibited_templates if is_prohibited else self.requirement_templates
@@ -131,12 +130,7 @@ class Facet:
return parameter_dict
def get_usage(self):
if self.minOccurs != 0:
return "required"
elif self.minOccurs == 0 and self.maxOccurs != 0:
return "optional"
elif self.maxOccurs == 0:
return "prohibited"
return self.cardinality
class Entity(Facet):
@@ -197,8 +191,8 @@ class Entity(Facet):
class Attribute(Facet):
def __init__(self, name="Name", value=None, minOccurs=None, maxOccurs=None, instructions=None):
self.parameters = ["name", "value", "@minOccurs", "@maxOccurs", "@instructions"]
def __init__(self, name="Name", value=None, cardinality="required", instructions=None):
self.parameters = ["name", "value", "@cardinality", "@instructions"]
self.applicability_templates = [
"Data where the {name} is {value}",
"Data where the {name} is provided",
@@ -211,7 +205,7 @@ class Attribute(Facet):
"The {name} shall not be {value}",
"The {name} shall not be provided",
]
super().__init__(name, value, minOccurs, maxOccurs, instructions)
super().__init__(name, value, cardinality, instructions)
def filter(
self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None]
@@ -242,7 +236,7 @@ class Attribute(Facet):
return results
def __call__(self, inst, logger=None):
if self.minOccurs == 0 and self.maxOccurs != 0:
if self.cardinality == "optional":
return AttributeResult(True)
if isinstance(self.name, str):
@@ -323,34 +317,31 @@ class Attribute(Facet):
reason = {"type": "VALUE", "actual": value}
break
if self.maxOccurs == 0:
if self.cardinality == "prohibited":
return AttributeResult(not is_pass, {"type": "PROHIBITED"})
return AttributeResult(is_pass, reason)
class Classification(Facet):
def __init__(self, value=None, system=None, uri=None, minOccurs=None, maxOccurs="unbounded", instructions=None):
self.parameters = ["value", "system", "@uri", "@minOccurs", "@maxOccurs", "@instructions"]
def __init__(self, value=None, system=None, uri=None, cardinality="required", instructions=None):
self.parameters = ["value", "system", "@uri", "@cardinality", "@instructions"]
self.applicability_templates = [
"Data having a {system} reference of {value}",
"Data classified using {system}",
"Data classified as {value}",
"Classified data",
]
self.requirement_templates = [
"Shall have a {system} reference of {value}",
"Shall be classified using {system}",
"Shall be classified as {value}",
"Shall be classified",
]
self.prohibited_templates = [
"Shall not have a {system} reference of {value}",
"Shall not be classified using {system}",
"Shall not be classified as {value}",
"Shall not be classified",
]
super().__init__(value, system, uri, minOccurs, maxOccurs, instructions)
super().__init__(value, system, uri, cardinality, instructions)
def filter(
self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None]
@@ -360,8 +351,8 @@ class Classification(Facet):
return ifc_file.by_type("IfcObjectDefinition")
def __call__(self, inst, logger=None):
if self.minOccurs == 0 and self.maxOccurs != 0:
return ClassificationResult(True)
if self.cardinality == "optional":
return ClassificationResult(True) # Is this really the correct behaviour?
leaf_references = ifcopenshell.util.classification.get_references(inst)
@@ -381,13 +372,13 @@ class Classification(Facet):
if not is_pass:
reason = {"type": "VALUE", "actual": values}
if is_pass and self.system:
if is_pass:
systems = [ifcopenshell.util.classification.get_classification(r).Name for r in references]
is_pass = any([self.system == s for s in systems])
if not is_pass:
reason = {"type": "SYSTEM", "actual": systems}
if self.maxOccurs == 0:
if self.cardinality == "prohibited":
return ClassificationResult(not is_pass, {"type": "PROHIBITED"})
return ClassificationResult(is_pass, reason)
@@ -398,11 +389,10 @@ class PartOf(Facet):
name="IFCWALL",
predefinedType=None,
relation=None,
minOccurs=None,
maxOccurs="unbounded",
cardinality="required",
instructions=None,
):
self.parameters = ["name", "predefinedType", "@relation", "@minOccurs", "@maxOccurs", "@instructions"]
self.parameters = ["name", "predefinedType", "@relation", "@cardinality", "@instructions"]
self.applicability_templates = [
"An element with an {relation} relationship with an {name}",
"An element with an {relation} relationship",
@@ -415,7 +405,7 @@ class PartOf(Facet):
"An element must not have an {relation} relationship with an {name}",
"An element must not have an {relation} relationship",
]
super().__init__(name, predefinedType, relation, minOccurs, maxOccurs, instructions)
super().__init__(name, predefinedType, relation, cardinality, instructions)
def filter(
self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None]
@@ -444,9 +434,6 @@ class PartOf(Facet):
return super().parse(xml)
def __call__(self, inst, logger=None):
if self.minOccurs == 0 and self.maxOccurs != 0:
return PartOfResult(True)
reason = None
if not self.relation:
is_pass = False
@@ -536,8 +523,14 @@ class PartOf(Facet):
nest = self.get_nested_whole(nest)
if not is_pass:
reason = {"type": "ENTITY", "actual": ancestors}
elif self.relation == "IFCRELVOIDSELEMENT":
building_element = self.get_voided_element(inst)
elif self.relation == "IFCRELVOIDSELEMENT IFCRELFILLSELEMENT":
if inst.is_a("IfcOpeningElement"):
building_element = self.get_voided_element(inst)
else:
building_element = None
opening = self.get_filled_opening(inst)
if opening:
building_element = self.get_voided_element(opening)
is_pass = building_element is not None
if not is_pass:
reason = {"type": "NOVALUE"}
@@ -551,23 +544,8 @@ class PartOf(Facet):
is_pass = True
if not is_pass:
reason = {"type": "ENTITY", "actual": building_element}
elif self.relation == "IFCRELFILLSELEMENT":
opening = self.filled_opening(inst)
is_pass = opening is not None
if not is_pass:
reason = {"type": "NOVALUE"}
if is_pass and self.name:
is_pass = False
if opening.is_a().upper() == self.name:
if self.predefinedType:
if ifcopenshell.util.element.get_predefined_type(opening) == self.predefinedType:
is_pass = True
else:
is_pass = True
if not is_pass:
reason = {"type": "ENTITY", "actual": opening}
if self.maxOccurs == 0:
if self.cardinality == "prohibited":
return PartOfResult(not is_pass, {"type": "PROHIBITED"})
return PartOfResult(is_pass, reason)
@@ -605,37 +583,35 @@ class Property(Facet):
def __init__(
self,
propertySet="Property_Set",
name="PropertyName",
baseName="PropertyName",
value=None,
datatype=None,
dataType=None,
uri=None,
minOccurs=None,
maxOccurs="unbounded",
cardinality="required",
instructions=None,
):
self.parameters = [
"propertySet",
"name",
"baseName",
"value",
"@datatype",
"@dataType",
"@uri",
"@minOccurs",
"@maxOccurs",
"@cardinality",
"@instructions",
]
self.applicability_templates = [
"Elements with {name} data of {value} in the dataset {propertySet}",
"Elements with {name} data in the dataset {propertySet}",
"Elements with {baseName} data of {value} in the dataset {propertySet}",
"Elements with {baseName} data in the dataset {propertySet}",
]
self.requirement_templates = [
"{name} data shall be {value} and in the dataset {propertySet}",
"{name} data shall be provided in the dataset {propertySet}",
"{baseName} data shall be {value} and in the dataset {propertySet}",
"{baseName} data shall be provided in the dataset {propertySet}",
]
self.prohibited_templates = [
"{name} data shall not be {value} and in the dataset {propertySet}",
"{name} data shall not be provided in the dataset {propertySet}",
"{baseName} data shall not be {value} and in the dataset {propertySet}",
"{baseName} data shall not be provided in the dataset {propertySet}",
]
super().__init__(propertySet, name, value, datatype, uri, minOccurs, maxOccurs, instructions)
super().__init__(propertySet, baseName, value, dataType, uri, cardinality, instructions)
def filter(
self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None]
@@ -651,7 +627,7 @@ class Property(Facet):
)
def __call__(self, inst, logger=None):
if self.minOccurs == 0 and self.maxOccurs != 0:
if self.cardinality == "optional":
return PropertyResult(True)
if isinstance(self.propertySet, str):
@@ -671,18 +647,18 @@ class Property(Facet):
props = {}
for pset_name, pset_props in psets.items():
props[pset_name] = {}
if isinstance(self.name, str):
prop = pset_props.get(self.name)
if isinstance(self.baseName, str):
prop = pset_props.get(self.baseName)
if prop == "UNKNOWN" and [
p
for p in self.get_properties(inst.wrapped_data.file.by_id(pset_props["id"]))
if p.Name == self.name
if p.Name == self.baseName
][0].NominalValue.is_a("IfcLogical"):
pass
elif prop is not None and prop != "":
props[pset_name][self.name] = prop
props[pset_name][self.baseName] = prop
else:
props[pset_name] = {k: v for k, v in pset_props.items() if k == self.name}
props[pset_name] = {k: v for k, v in pset_props.items() if k == self.baseName}
if not bool(props[pset_name]):
is_pass = False
@@ -701,9 +677,9 @@ class Property(Facet):
elif prop_entity.is_a("IfcPropertySingleValue"):
data_type = prop_entity.NominalValue.is_a()
if data_type.lower() != self.datatype.lower():
if self.dataType and data_type.lower() != self.dataType.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype}
reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType}
break
unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file)
@@ -720,9 +696,9 @@ class Property(Facet):
prop_schema = prop_entity.wrapped_data.declaration().as_entity()
data_type = prop_schema.attribute_by_index(3).type_of_attribute().declared_type().name()
if data_type.lower() != self.datatype.lower():
if self.dataType and data_type.lower() != self.dataType.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype}
reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType}
break
unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file)
@@ -740,9 +716,9 @@ class Property(Facet):
reason = {"type": "NOVALUE"}
break
data_type = prop_entity.EnumerationValues[0].is_a()
if data_type.lower() != self.datatype.lower():
if self.dataType and data_type.lower() != self.dataType.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype}
reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType}
break
elif prop_entity.is_a("IfcPropertyListValue"):
if not prop_entity.ListValues:
@@ -750,9 +726,9 @@ class Property(Facet):
reason = {"type": "NOVALUE"}
break
data_type = prop_entity.ListValues[0].is_a()
if data_type.lower() != self.datatype.lower():
if self.dataType and data_type.lower() != self.dataType.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype}
reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType}
break
unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file)
if unit:
@@ -773,9 +749,9 @@ class Property(Facet):
if value is not None:
data_type = value.is_a()
values.append(value.wrappedValue)
if data_type.lower() != self.datatype.lower():
if self.dataType and data_type.lower() != self.dataType.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype}
reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType}
break
unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file)
if unit:
@@ -798,7 +774,7 @@ class Property(Facet):
if not column_values:
continue
data_type = column_values[0].is_a()
if data_type.lower() == self.datatype.lower():
if self.dataType and data_type.lower() == self.dataType.lower():
column_values = [v.wrappedValue for v in column_values]
unit = units[f"{attribute}Unit"]
if unit:
@@ -815,7 +791,7 @@ class Property(Facet):
values.extend(column_values)
if not values:
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype}
reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType}
break
props[pset_name][prop_entity.Name] = values
else:
@@ -868,7 +844,7 @@ class Property(Facet):
reason = {"type": "VALUE", "actual": value}
break
if self.maxOccurs == 0:
if self.cardinality == "prohibited":
return PropertyResult(not is_pass, {"type": "PROHIBITED"})
return PropertyResult(is_pass, reason)
@@ -888,8 +864,8 @@ class Property(Facet):
class Material(Facet):
def __init__(self, value=None, uri=None, minOccurs=None, maxOccurs="unbounded", instructions=None):
self.parameters = ["value", "@uri", "@minOccurs", "@maxOccurs", "@instructions"]
def __init__(self, value=None, uri=None, cardinality="required", instructions=None):
self.parameters = ["value", "@uri", "@cardinality", "@instructions"]
self.applicability_templates = [
"All data with a {value} material",
"All data with a material",
@@ -902,7 +878,7 @@ class Material(Facet):
"Shall not have a material of {value}",
"Shall not have a material",
]
super().__init__(value, uri, minOccurs, maxOccurs, instructions)
super().__init__(value, uri, cardinality, instructions)
def filter(
self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None]
@@ -912,7 +888,7 @@ class Material(Facet):
return ifc_file.by_type("IfcObjectDefinition")
def __call__(self, inst, logger=None):
if self.minOccurs == 0 and self.maxOccurs != 0:
if self.cardinality == "optional":
return MaterialResult(True)
material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True)
@@ -963,7 +939,7 @@ class Material(Facet):
if not is_pass:
reason = {"type": "VALUE", "actual": values}
if self.maxOccurs == 0:
if self.cardinality == "prohibited":
return MaterialResult(not is_pass, {"type": "PROHIBITED"})
return MaterialResult(is_pass, reason)
@@ -1108,7 +1084,7 @@ class PropertyResult(Result):
elif self.reason["type"] == "NOVALUE":
return "The property set does not contain the required property"
elif self.reason["type"] == "DATATYPE":
return f"The property's data type \"{str(self.reason['actual'])}\" does not match the required data type of \"{str(self.reason['datatype'])}\""
return f"The property's data type \"{str(self.reason['actual'])}\" does not match the required data type of \"{str(self.reason['dataType'])}\""
elif self.reason["type"] == "VALUE":
if isinstance(self.reason["actual"], list):
if len(self.reason["actual"]) == 1:
+31 -20
View File
@@ -21,7 +21,18 @@ import datetime
from xmlschema import XMLSchema
from xmlschema import etree_tostring
from xml.etree import ElementTree as ET
from .facet import Facet, Entity, Attribute, Classification, Property, PartOf, Material, Restriction, get_pset, get_psets
from .facet import (
Facet,
Entity,
Attribute,
Classification,
Property,
PartOf,
Material,
Restriction,
get_pset,
get_psets,
)
from typing import List, Set
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -89,7 +100,7 @@ class Ids:
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd",
"info": info,
"specifications": {"specification": []},
}
@@ -164,7 +175,7 @@ class Specification:
"applicability": {},
"requirements": {},
}
for attribute in ["identifier", "description", "instructions", "minOccurs", "maxOccurs"]:
for attribute in ["identifier", "description", "instructions"]:
value = getattr(self, attribute)
if value is not None:
results[f"@{attribute}"] = value
@@ -181,20 +192,25 @@ class Specification:
for facet_type in ("entity", "partOf", "classification", "attribute", "property", "material"):
if facet_type in facets:
results[clause_type][facet_type] = facets[facet_type]
if clause_type == "applicability":
for attribute in ["minOccurs", "maxOccurs"]:
value = getattr(self, attribute)
if value is not None:
results[clause_type][f"@{attribute}"] = value
return results
def parse(self, ids_dict):
self.name = ids_dict.get("@name", "")
self.description = ids_dict.get("@description", "")
self.instructions = ids_dict.get("@instructions", "")
self.minOccurs = ids_dict["@minOccurs"]
self.maxOccurs = ids_dict["@maxOccurs"]
self.minOccurs = ids_dict.get("applicability", {}).get("@minOccurs", 0)
self.maxOccurs = ids_dict.get("applicability", {}).get("@maxOccurs", "unbounded")
self.ifcVersion = ids_dict["@ifcVersion"]
self.applicability = (
self.parse_clause(ids_dict["applicability"]) if ids_dict.get("applicability") is not None else []
self.parse_clause(ids_dict["applicability"]) if ids_dict.get("applicability", None) is not None else []
)
self.requirements = (
self.parse_clause(ids_dict["requirements"]) if ids_dict.get("requirements") is not None else []
self.parse_clause(ids_dict["requirements"]) if ids_dict.get("requirements", None) is not None else []
)
return self
@@ -216,7 +232,7 @@ class Specification:
self.failed_entities: Set[Entity] = set()
for facet in self.requirements:
facet.status = None
facet.failed_entities.clear()
facet.failures.clear()
self.status = None
def validate(self, ifc_file, filter_version=False):
@@ -243,22 +259,17 @@ class Specification:
self.applicable_entities.append(element)
for facet in self.requirements:
result = facet(element)
if self.maxOccurs == 0:
prohibited = bool(result)
else:
prohibited = not bool(result)
if prohibited:
if not bool(result):
self.failed_entities.add(element)
facet.failed_entities.append(element)
facet.failed_reasons.append(str(result))
facet.failures.append({"element": element, "reason": str(result)})
for facet in self.requirements:
if facet.minOccurs != 0:
facet.status = not bool(facet.failed_entities)
elif facet.minOccurs == 0 and facet.maxOccurs != 0:
if facet.cardinality == "required":
facet.status = not bool(facet.failures)
elif facet.cardinality == "optional":
facet.status = True
elif facet.maxOccurs == 0:
facet.status = bool(facet.failed_entities)
elif facet.cardinality == "prohibited":
facet.status = bool(facet.failures)
self.status = True
if self.minOccurs != 0:
+78 -26
View File
@@ -1,5 +1,5 @@
<!-- June 20, 2023 - DRAFT -->
<xs:schema xmlns:ids="http://standards.buildingsmart.org/IDS" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:altova="http://www.altova.com/xml-schema-extensions" targetNamespace="http://standards.buildingsmart.org/IDS" elementFormDefault="qualified" attributeFormDefault="unqualified" version="0.9.6">
<!-- Draft - Do not use in production -->
<xs:schema xmlns:ids="http://standards.buildingsmart.org/IDS" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:altova="http://www.altova.com/xml-schema-extensions" targetNamespace="http://standards.buildingsmart.org/IDS" elementFormDefault="qualified" attributeFormDefault="unqualified" version="0.9.7">
<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"/>
<xs:import namespace="http://www.w3.org/2001/XMLSchema" schemaLocation="http://www.w3.org/2001/XMLSchema.xsd"/>
<xs:import namespace="http://www.w3.org/2001/XMLSchema-instance" schemaLocation="http://www.w3.org/2001/XMLSchema-instance"/>
@@ -40,20 +40,20 @@
<xs:choice minOccurs="1">
<!-- place for potential additional rules for idsValue -->
<xs:element name="simpleValue" type="xs:string" minOccurs="1" maxOccurs="1"/>
<xs:element ref="xs:restriction" minOccurs="1" maxOccurs="unbounded"/>
<xs:element ref="xs:restriction" minOccurs="1" maxOccurs="1"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="classificationType">
<xs:sequence>
<xs:element name="value" type="ids:idsValue" minOccurs="0"/>
<xs:element name="system" type="ids:idsValue" minOccurs="0"/>
<xs:element name="system" type="ids:idsValue" minOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="partOfType">
<xs:sequence>
<xs:element name="entity" type="ids:entityType" minOccurs="1"/>
</xs:sequence>
<xs:attribute name="relation" type="ids:relations"/>
<xs:attribute name="relation" type="ids:relations" use="optional" />
</xs:complexType>
<xs:complexType name="applicabilityType">
<xs:sequence>
@@ -62,26 +62,53 @@
<xs:element name="classification" type="ids:classificationType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="attribute" type="ids:attributeType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="property" type="ids:propertyType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="material" type="ids:materialType" minOccurs="0"/>
<xs:element name="material" type="ids:materialType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<!-- Please note there is an implementation agreement on the use of minOccurs and maxOccurs.
Valid values are:
a. 0 to unbounded, meaning Optional
b. 1 to unbounded, meaning Required
c. 0 to 0, meaning Prohibited
-->
<xs:attributeGroup ref="xs:occurs"/>
</xs:complexType>
<xs:complexType name="propertyType">
<xs:sequence>
<xs:element name="propertySet" type="ids:idsValue"/>
<xs:element name="name" type="ids:idsValue"/>
<xs:element name="value" type="ids:idsValue" minOccurs="0"/>
<xs:element name="baseName" type="ids:idsValue">
<xs:annotation>
<xs:documentation>
the moniker 'baseName' is chosen to clarify that the data needs to reference the property name as stored in the IFC file,
which might differ from the multiple language-dependent presentations (e.g. 'FireRating' vs. 'Fire rating').
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="value" type="ids:idsValue" minOccurs="0">
<xs:annotation>
<xs:documentation>
Depending on the dataType attribute, values are expressed in the default unit documented at
https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md, and unit conversion might be required.
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="datatype" use="required">
<xs:attribute name="dataType" type="ids:upperCaseName" use="optional">
<xs:annotation>
<xs:documentation>This is the name of an IFC Defined Type. See the full list for IFC 4 on https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/link/alphabeticalorder-defined-types.htm Documentation and default units on https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md</xs:documentation>
<xs:documentation>This is the name of an IFC Defined Type, all uppercase.</xs:documentation>
</xs:annotation>
<!-- renamed 'measure' to data type to better represent reality -->
</xs:attribute>
</xs:complexType>
<xs:complexType name="attributeType">
<xs:sequence>
<xs:element name="name" type="ids:idsValue"/>
<xs:element name="value" type="ids:idsValue" minOccurs="0"/>
<xs:element name="value" type="ids:idsValue" minOccurs="0">
<xs:annotation>
<xs:documentation>
Depending on the IFC type of the attribute, values are expressed in the default unit documented at
https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md, and unit conversion might be required.
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="materialType">
@@ -98,6 +125,13 @@
<xs:complexType>
<xs:complexContent>
<xs:extension base="ids:entityType">
<!--
Contrary to other requirements facet extensions, cardinality is not available in the entityType facet when used for requirements.
Its cardinality state is always considered to be "required".
Constraining the acceptable values is achieved by specifying criteria via with xs:Enumeration and xs:Pattern, rather than the negative form.
This is possible because the list of options is finite and mandated by the IFC schema, so prohibited constraints are superfluous.
This choice allows for improved user experience in the editors.
-->
<xs:attribute name="instructions" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool.</xs:documentation>
@@ -111,7 +145,7 @@
<xs:complexType>
<xs:complexContent>
<xs:extension base="ids:partOfType">
<xs:attributeGroup ref="xs:occurs"/>
<xs:attribute name="cardinality" type="ids:simpleCardinality" use="optional" default="required"/>
<xs:attribute name="instructions" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool.</xs:documentation>
@@ -126,7 +160,7 @@
<xs:complexContent>
<xs:extension base="ids:classificationType">
<xs:attribute name="uri" type="xs:anyURI" use="optional"/>
<xs:attributeGroup ref="xs:occurs"/>
<xs:attribute name="cardinality" type="ids:conditionalCardinality" use="optional" default="required"/>
<xs:attribute name="instructions" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool.</xs:documentation>
@@ -140,6 +174,7 @@
<xs:complexType>
<xs:complexContent>
<xs:extension base="ids:attributeType">
<xs:attribute name="cardinality" type="ids:conditionalCardinality" use="optional" default="required"/>
<xs:attribute name="instructions" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool.</xs:documentation>
@@ -154,7 +189,7 @@
<xs:complexContent>
<xs:extension base="ids:propertyType">
<xs:attribute name="uri" type="xs:anyURI" use="optional"/>
<xs:attributeGroup ref="xs:occurs"/>
<xs:attribute name="cardinality" type="ids:conditionalCardinality" use="optional" default="required"/>
<xs:attribute name="instructions" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool.</xs:documentation>
@@ -164,12 +199,12 @@
</xs:complexContent>
</xs:complexType>
</xs:element>
<xs:element name="material" minOccurs="0">
<xs:element name="material" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:complexContent>
<xs:extension base="ids:materialType">
<xs:attribute name="uri" type="xs:anyURI" use="optional"/>
<xs:attributeGroup ref="xs:occurs"/>
<xs:attribute name="cardinality" type="ids:conditionalCardinality" use="optional" default="required"/>
<xs:attribute name="instructions" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool.</xs:documentation>
@@ -195,7 +230,6 @@
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attributeGroup ref="xs:occurs"/>
<xs:attribute name="ifcVersion" use="required">
<xs:simpleType>
<xs:list>
@@ -210,7 +244,7 @@
</xs:list>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="identifier" type="xs:string">
<xs:attribute name="identifier" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can provide an identifier to the specification. This is intended to be a machine readable identifier. Beware: because of the possibility to combine different 'specification' elements from several ids files this cannot be enforced/assumed as (global) unique.</xs:documentation>
</xs:annotation>
@@ -229,12 +263,30 @@
</xs:complexType>
<xs:simpleType name="relations">
<xs:restriction base="xs:string">
<xs:enumeration value="IFCRELAGGREGATES"/>
<xs:enumeration value="IFCRELASSIGNSTOGROUP"/>
<xs:enumeration value="IFCRELCONTAINEDINSPATIALSTRUCTURE"/>
<xs:enumeration value="IFCRELNESTS"/>
<xs:enumeration value="IFCRELVOIDSELEMENT"/>
<xs:enumeration value="IFCRELFILLSELEMENT"/>
<xs:enumeration value="IFCRELAGGREGATES"/>
<xs:enumeration value="IFCRELASSIGNSTOGROUP"/>
<xs:enumeration value="IFCRELCONTAINEDINSPATIALSTRUCTURE"/>
<xs:enumeration value="IFCRELNESTS"/>
<xs:enumeration value="IFCRELVOIDSELEMENT IFCRELFILLSELEMENT"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
<xs:simpleType name="upperCaseName">
<xs:restriction base="xs:normalizedString">
<xs:pattern value="[A-Z]+"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="simpleCardinality">
<xs:restriction base="xs:string">
<xs:enumeration value="required"/>
<xs:enumeration value="prohibited"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="conditionalCardinality">
<xs:restriction base="xs:string">
<xs:enumeration value="required"/>
<xs:enumeration value="prohibited"/>
<xs:enumeration value="optional"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
+20 -20
View File
@@ -105,19 +105,19 @@ class Console(Reporter):
for requirement in specification.requirements:
self.set_style("reset")
self.set_style("red") if requirement.failed_entities else self.set_style("green")
self.set_style("red") if requirement.failures else self.set_style("green")
self.print(" " * 8 + requirement.to_string("requirement", specification, requirement))
self.set_style("reset")
for i, element in enumerate(requirement.failed_entities[0:10]):
for failure in requirement.failures[0:10]:
self.print(" " * 12, end="")
self.report_reason(requirement.failed_reasons[i], element)
if len(requirement.failed_entities) > 10:
self.print(" " * 12 + f"... {len(requirement.failed_entities)} in total ...")
self.report_reason(failure)
if len(requirement.failures) > 10:
self.print(" " * 12 + f"... {len(requirement.failures)} in total ...")
self.set_style("reset")
def report_reason(self, reason, element):
def report_reason(self, failure):
is_bold = False
for substring in reason.split('"'):
for substring in failure["reason"].split('"'):
if is_bold:
self.set_style("purple")
else:
@@ -125,7 +125,7 @@ class Console(Reporter):
self.print(substring, end="")
is_bold = not is_bold
self.set_style("grey")
self.print(" - " + str(element))
self.print(" - " + str(failure["element"]))
self.set_style("reset")
def set_style(self, *colours):
@@ -212,7 +212,7 @@ class Json(Reporter):
total_checks_pass = 0
requirements = []
for requirement in specification.requirements:
total_fail = len(requirement.failed_entities)
total_fail = len(requirement.failures)
total_pass = total_applicable - total_fail
percent_pass = math.floor((total_pass / total_applicable) * 100) if total_applicable else "N/A"
total_checks += total_applicable
@@ -254,18 +254,18 @@ class Json(Reporter):
def report_failed_entities(self, requirement):
return [
{
"reason": requirement.failed_reasons[i],
"element": str(e),
"element_type": str(ifcopenshell.util.element.get_type(e)),
"class": e.is_a(),
"predefined_type": ifcopenshell.util.element.get_predefined_type(e),
"name": getattr(e, "Name", None),
"description": getattr(e, "Description", None),
"id": e.id(),
"global_id": getattr(e, "GlobalId", None),
"tag": getattr(e, "Tag", None),
"reason": f["reason"],
"element": str(f["element"]),
"element_type": str(ifcopenshell.util.element.get_type(f["element"])),
"class": f["element"].is_a(),
"predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]),
"name": getattr(f["element"], "Name", None),
"description": getattr(f["element"], "Description", None),
"id": f["element"].id(),
"global_id": getattr(f["element"], "GlobalId", None),
"tag": getattr(f["element"], "Tag", None),
}
for i, e in enumerate(requirement.failed_entities)
for f in requirement.failures
]
def to_string(self):
+119 -129
View File
@@ -37,9 +37,9 @@ def run(name, *, facet, inst, expected):
class TestEntity:
def test_creating_an_entity_facet(self):
facet = Entity(name="IfcName")
assert facet.asdict() == {"name": {"simpleValue": "IfcName"}}
assert facet.asdict("applicability") == {"name": {"simpleValue": "IfcName"}}
facet = Entity(name="IfcName", predefinedType="predefinedType", instructions="instructions")
assert facet.asdict() == {
assert facet.asdict("requirement") == {
"name": {"simpleValue": "IfcName"},
"predefinedType": {"simpleValue": "predefinedType"},
"@instructions": "instructions",
@@ -223,17 +223,14 @@ class TestEntity:
class TestAttribute:
def test_creating_an_attribute_facet(self):
attribute = Attribute(name="name")
assert attribute.asdict() == {"name": {"simpleValue": "name"}}
assert attribute.asdict("applicability") == {"name": {"simpleValue": "name"}}
attribute = Attribute(name="name", value="value")
assert attribute.asdict() == {"name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}}
attribute = Attribute(
name="name", value="value", minOccurs="0", maxOccurs="unbounded", instructions="instructions"
)
assert attribute.asdict() == {
assert attribute.asdict("applicability") == {"name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}}
attribute = Attribute(name="name", value="value", cardinality="required", instructions="instructions")
assert attribute.asdict("requirement") == {
"name": {"simpleValue": "name"},
"value": {"simpleValue": "value"},
"@minOccurs": "0",
"@maxOccurs": "unbounded",
"@cardinality": "required",
"@instructions": "instructions",
}
@@ -258,12 +255,12 @@ class TestAttribute:
element = ifc.createIfcWall(Name="Foobar")
run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True)
# facet = Attribute(name="Name", minOccurs=0, maxOccurs=0)
# run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False)
# facet = Attribute(name="Name", minOccurs=0)
# run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True)
# facet = Attribute(name="Rabbit", minOccurs=0)
# run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True)
facet = Attribute(name="Name", cardinality="prohibited")
run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False)
facet = Attribute(name="Name", cardinality="optional")
run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True)
facet = Attribute(name="Rabbit", cardinality="optional")
run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True)
ifc = ifcopenshell.file()
facet = Attribute(name="Name")
@@ -668,26 +665,26 @@ class TestAttribute:
class TestClassification:
def test_creating_a_classification_facet(self):
facet = Classification()
assert facet.asdict() == {
"@maxOccurs": "unbounded"
}
facet = Classification(system="system")
assert facet.asdict("requirement") == {"system": {"simpleValue": "system"}, "@cardinality": "required"}
facet = Classification(value="value", system="system")
assert facet.asdict() == {"value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}, "@maxOccurs": "unbounded" }
assert facet.asdict("requirement") == {
"value": {"simpleValue": "value"},
"system": {"simpleValue": "system"},
"@cardinality": "required",
}
facet = Classification(
value="value",
system="system",
uri="https://test.com",
minOccurs="0",
maxOccurs="unbounded",
cardinality="required",
instructions="instructions",
)
assert facet.asdict() == {
assert facet.asdict("requirement") == {
"value": {"simpleValue": "value"},
"system": {"simpleValue": "system"},
"@uri": "https://test.com",
"@minOccurs": "0",
"@maxOccurs": "unbounded",
"@cardinality": "required",
"@instructions": "instructions",
}
@@ -729,29 +726,29 @@ class TestClassification:
"classification.add_reference", ifc, product=material, reference=ref1, classification=system_a
)
facet = Classification()
facet = Classification(system="Foobar")
run(
"A classification facet with no data matches any classification 1/2",
"A classification facet with no value matches any classification 1/2",
facet=facet,
inst=element0,
expected=False,
)
run(
"A classification facet with no data matches any classification 2/2",
"A classification facet with no value matches any classification 2/2",
facet=facet,
inst=element1,
expected=True,
)
run("A required facet checks all parameters as normal", facet=facet, inst=element1, expected=True)
facet = Classification(minOccurs=0, maxOccurs=0)
facet = Classification(system="Foobar", cardinality="prohibited")
run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element1, expected=False)
facet = Classification(minOccurs=0)
facet = Classification(system="Foobar", cardinality="optional")
run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element0, expected=True)
facet = Classification(minOccurs=0)
facet = Classification(system="Foobar", cardinality="optional")
run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element1, expected=True)
facet = Classification(value="1")
facet = Classification(system="Foobar", value="1")
run(
"Values should match exactly if lightweight classifications are used",
facet=facet,
@@ -759,7 +756,7 @@ class TestClassification:
expected=True,
)
facet = Classification(value="2")
facet = Classification(system="Foobar", value="2")
run(
"Values match subreferences if full classifications are used (e.g. EF_25_10 should match EF_25_10_25, EF_25_10_30, etc)",
facet=facet,
@@ -767,7 +764,7 @@ class TestClassification:
expected=True,
)
facet = Classification(value="1")
facet = Classification(system="Foobar", value="1")
run(
"Non-rooted resources that have external classification references should also pass",
facet=facet,
@@ -783,7 +780,7 @@ class TestClassification:
run("Systems should match exactly 5/5", facet=facet, inst=element22, expected=True)
restriction = Restriction(options={"pattern": "1.*"})
facet = Classification(value=restriction)
facet = Classification(system="Foobar", value=restriction)
run("Restrictions can be used for values 1/3", facet=facet, inst=element1, expected=True)
run("Restrictions can be used for values 2/3", facet=facet, inst=element11, expected=True)
run("Restrictions can be used for values 3/3", facet=facet, inst=element22, expected=False)
@@ -824,40 +821,38 @@ class TestClassification:
"classification.add_reference", ifc, product=wall_type, reference=refx, classification=system_b
)
facet = Classification(value="11")
facet = Classification(system="Foobar", value="11")
run("Occurrences override the type classification per system 1/3", facet=facet, inst=wall, expected=True)
facet = Classification(value="22")
facet = Classification(system="Foobar", value="22")
run("Occurrences override the type classification per system 2/3", facet=facet, inst=wall, expected=False)
facet = Classification(value="X")
facet = Classification(system="Foobaz", value="X")
run("Occurrences override the type classification per system 3/3", facet=facet, inst=wall, expected=True)
class TestProperty:
def test_creating_a_property_facet(self):
facet = Property()
assert facet.asdict() == {
assert facet.asdict("requirement") == {
"propertySet": {"simpleValue": "Property_Set"},
"name": {"simpleValue": "PropertyName"},
"@maxOccurs": "unbounded"
"baseName": {"simpleValue": "PropertyName"},
"@cardinality": "required",
}
facet = Property(
propertySet="propertySet",
name="name",
baseName="baseName",
value="value",
datatype="datatype",
dataType="dataType",
uri="https://test.com",
minOccurs="0",
maxOccurs="unbounded",
cardinality="required",
instructions="instructions",
)
assert facet.asdict() == {
assert facet.asdict("requirement") == {
"propertySet": {"simpleValue": "propertySet"},
"name": {"simpleValue": "name"},
"baseName": {"simpleValue": "baseName"},
"value": {"simpleValue": "value"},
"@datatype": "datatype",
"@dataType": "DATATYPE",
"@uri": "https://test.com",
"@minOccurs": "0",
"@maxOccurs": "unbounded",
"@cardinality": "required",
"@instructions": "instructions",
}
@@ -866,7 +861,7 @@ class TestProperty:
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
run("Elements with no properties always fail", facet=facet, inst=element, expected=False)
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
@@ -878,27 +873,27 @@ class TestProperty:
run("A name check will match any property with any string value", facet=facet, inst=element, expected=True)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL", minOccurs=0, maxOccurs=0)
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL", cardinality="prohibited")
run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL", minOccurs=0)
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL", cardinality="optional")
run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Bar", datatype="IFCLABEL", minOccurs=0)
facet = Property(propertySet="Foo_Bar", baseName="Bar", dataType="IFCLABEL", cardinality="optional")
run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ""})
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLOGICAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLOGICAL")
run("An empty string is considered falsey and will not pass", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLogical("UNKNOWN")})
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCDURATION")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCDURATION")
run("A logical unknown is considered falsey and will not pass", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P0D")})
run("A zero duration will pass", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCBOOLEAN")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCBOOLEAN")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(True)})
run("A property set to true will pass a name check", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": False})
@@ -910,7 +905,7 @@ class TestProperty:
)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Bar", dataType="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
@@ -920,55 +915,55 @@ class TestProperty:
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"})
run("Specifying a value fails against different values", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="♫Don'tÄrgerhôtelЊет", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="♫Don'tÄrgerhôtelЊет", dataType="IFCLABEL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "♫Don'tÄrgerhôtelЊет"})
run("Non-ascii characters are treated without encoding", facet=facet, inst=element, expected=True)
identifier = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345"
facet = Property(
propertySet="Foo_Bar", name="Foo", value=identifier + "_extra_characters", datatype="IFCIDENTIFIER"
propertySet="Foo_Bar", baseName="Foo", value=identifier + "_extra_characters", dataType="IFCIDENTIFIER"
)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcIdentifier(identifier)})
run("IDS does not handle string truncation such as for identifiers", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1", dataType="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "1"})
run("A number specified as a string is treated as a string", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IFCINTEGER")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42", dataType="IFCINTEGER")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcInteger(42)})
run("Integer values are checked using type casting 1/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IFCINTEGER")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.", dataType="IFCINTEGER")
run("Integer values are checked using type casting 2/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IFCINTEGER")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.0", dataType="IFCINTEGER")
run("Integer values are checked using type casting 3/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IFCINTEGER")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.3", dataType="IFCINTEGER")
run("Integer values are checked using type casting 4/4", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42", dataType="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0)})
run("Real values are checked using type casting 1/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.0", dataType="IFCREAL")
run("Real values are checked using type casting 2/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.3", dataType="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)})
run("Real values are checked using type casting 3/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42,3", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42,3", dataType="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)})
run("Only specifically formatted numbers are allowed 1/4", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="123,4.5", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="123,4.5", dataType="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(1234.5)})
run("Only specifically formatted numbers are allowed 2/4", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345e3", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1.2345e3", dataType="IFCREAL")
run("Only specifically formatted numbers are allowed 3/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345E3", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1.2345E3", dataType="IFCREAL")
run("Only specifically formatted numbers are allowed 4/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IFCREAL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.", dataType="IFCREAL")
ifcopenshell.api.run(
"pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 + 1e-6))}
)
@@ -986,15 +981,15 @@ class TestProperty:
)
run("Floating point numbers are compared with a 1e-6 tolerance 4/4", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="TRUE", datatype="IFCBOOLEAN")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="TRUE", dataType="IFCBOOLEAN")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(False)})
run("Booleans must be specified as uppercase strings 1/3", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="FALSE", datatype="IFCBOOLEAN")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="FALSE", dataType="IFCBOOLEAN")
run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="False", datatype="IFCBOOLEAN")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="False", dataType="IFCBOOLEAN")
run("Booleans must be specified as uppercase strings 3/3", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="2022-01-01", datatype="IFCDATE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2022-01-01", dataType="IFCDATE")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDate("2022-01-01")})
run("Dates are treated as strings 1/2", facet=facet, inst=element, expected=True)
ifcopenshell.api.run(
@@ -1002,7 +997,7 @@ class TestProperty:
)
run("Dates are treated as strings 2/2", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="PT16H", datatype="IFCDURATION")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="PT16H", dataType="IFCDURATION")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("PT16H")})
run("Durations are treated as strings 1/2", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P2D")})
@@ -1019,11 +1014,11 @@ class TestProperty:
properties={"Status": ["EXISTING", "DEMOLISH"]},
pset_template=pset_template,
)
facet = Property(propertySet="Pset_WallCommon", name="Status", value="EXISTING", datatype="IFCLABEL")
facet = Property(propertySet="Pset_WallCommon", baseName="Status", value="EXISTING", dataType="IFCLABEL")
run("Any matching value in an enumerated property will pass 1/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Pset_WallCommon", name="Status", value="DEMOLISH", datatype="IFCLABEL")
facet = Property(propertySet="Pset_WallCommon", baseName="Status", value="DEMOLISH", dataType="IFCLABEL")
run("Any matching value in an enumerated property will pass 2/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Pset_WallCommon", name="Status", value="NEW", datatype="IFCLABEL")
facet = Property(propertySet="Pset_WallCommon", baseName="Status", value="NEW", dataType="IFCLABEL")
run("Any matching value in an enumerated property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1033,11 +1028,11 @@ class TestProperty:
Name="Foo", ListValues=[ifc.createIfcLabel("X"), ifc.createIfcLabel("Y")]
)
pset.HasProperties = [list_property]
facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="X", dataType="IFCLABEL")
run("Any matching value in a list property will pass 1/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Y", dataType="IFCLABEL")
run("Any matching value in a list property will pass 2/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="Z", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Z", dataType="IFCLABEL")
run("Any matching value in a list property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1050,13 +1045,13 @@ class TestProperty:
SetPointValue=ifc.createIfcLengthMeasure(3000),
)
pset.HasProperties = [bounded_property]
facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1", dataType="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 1/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="5", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="5", dataType="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 2/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="3", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="3", dataType="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 3/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2", dataType="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 4/4", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1066,18 +1061,18 @@ class TestProperty:
Name="Foo", DefiningValues=[ifc.createIfcLabel("X")], DefinedValues=[ifc.createIfcLengthMeasure(1000)]
)
pset.HasProperties = [table_property]
facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="X", dataType="IFCLABEL")
run("Any matching value in a table property will pass 1/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1", dataType="IFCLENGTHMEASURE")
run("Any matching value in a table property will pass 2/3", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Y", dataType="IFCLABEL")
run("Any matching value in a table property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
pset.HasProperties = [ifc.createIfcPropertyReferenceValue(Name="Foo")]
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL")
run("Reference properties are treated as objects and not supported", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1096,21 +1091,21 @@ class TestProperty:
RelatingPropertyDefinition=pset,
)
facet = Property(
propertySet="Foo_Bar", name="PanelOperation", value="SWINGING", datatype="IFCDOORPANELOPERATIONENUM"
propertySet="Foo_Bar", baseName="PanelOperation", value="SWINGING", dataType="IFCDOORPANELOPERATIONENUM"
)
run("Predefined properties are supported but discouraged 1/2", facet=facet, inst=element, expected=True)
facet = Property(
propertySet="Foo_Bar", name="PanelOperation", value="SWONGING", datatype="IFCDOORPANELOPERATIONENUM"
propertySet="Foo_Bar", baseName="PanelOperation", value="SWONGING", dataType="IFCDOORPANELOPERATIONENUM"
)
run("Predefined properties are supported but discouraged 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLENGTHMEASURE")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
qto = ifcopenshell.api.run("pset.add_qto", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_qto", ifc, qto=qto, properties={"Foo": ifc.createIfcLengthMeasure(42)})
run("A name check will match any quantity with any value", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCAREAMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCAREAMEASURE")
run("Quantities must also match the appropriate measure", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1119,9 +1114,9 @@ class TestProperty:
complex_property = ifc.createIfcComplexProperty(Name="Foo", UsageName="RabbitAgilityTraining")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=complex_property, properties={"Rabbits": "Awesome"})
pset.HasProperties = [complex_property]
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL")
run("Complex properties are not supported 1/2", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo", name="Rabbits", datatype="IFCLABEL")
facet = Property(propertySet="Foo", baseName="Rabbits", dataType="IFCLABEL")
run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1132,14 +1127,14 @@ class TestProperty:
"pset.edit_qto", ifc, qto=complex_quantity, properties={"MyLength": ifc.createIfcLengthMeasure(42)}
)
qto.Quantities = [complex_quantity]
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLENGTHMEASURE")
run("Complex properties are not supported 1/2", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo", name="MyLength", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo", baseName="MyLength", dataType="IFCLENGTHMEASURE")
run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
restriction = Restriction(options={"pattern": "Foo_.*"})
facet = Property(propertySet=restriction, name="Foo", datatype="IFCLABEL")
facet = Property(propertySet=restriction, baseName="Foo", dataType="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
@@ -1152,7 +1147,7 @@ class TestProperty:
ifc = self.setup_ifc()
restriction = Restriction(options={"pattern": "Foo.*"})
facet = Property(propertySet="Foo_Bar", name=restriction, value="x", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName=restriction, value="x", dataType="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x"})
@@ -1165,7 +1160,7 @@ class TestProperty:
ifc = self.setup_ifc()
restriction1 = Restriction(options={"pattern": "Foo.*"})
restriction2 = Restriction(options={"enumeration": ["x", "y"]})
facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2, datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName=restriction1, value=restriction2, dataType="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"})
@@ -1184,7 +1179,7 @@ class TestProperty:
)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCTIMEMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2", dataType="IFCTIMEMEASURE")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcMassMeasure(2)})
@@ -1193,7 +1188,7 @@ class TestProperty:
run("Measures are used to specify an IFC data type 2/2", facet=facet, inst=element, expected=True)
ifc = self.setup_ifc()
facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCLENGTHMEASURE")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2", dataType="IFCLENGTHMEASURE")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2)})
@@ -1217,7 +1212,7 @@ class TestProperty:
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL")
run("Properties can be inherited from the type 1/2", facet=facet, inst=wall, expected=True)
run("Properties can be inherited from the type 2/2", facet=facet, inst=wall_type, expected=True)
@@ -1229,7 +1224,7 @@ class TestProperty:
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"})
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IFCLABEL")
facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Bar", dataType="IFCLABEL")
run("Properties can be overriden by an occurrence 1/2", facet=facet, inst=wall, expected=True)
run("Properties can be overriden by an occurrence 2/2", facet=facet, inst=wall_type, expected=False)
@@ -1248,15 +1243,12 @@ class TestProperty:
class TestMaterial:
def test_creating_a_material_facet(self):
facet = Material()
assert facet.asdict() == {"@maxOccurs": "unbounded"}
facet = Material(
value="value", uri="https://test.com", minOccurs="0", maxOccurs="unbounded", instructions="instructions"
)
assert facet.asdict() == {
assert facet.asdict("requirement") == {"@cardinality": "required"}
facet = Material(value="value", uri="https://test.com", cardinality="required", instructions="instructions")
assert facet.asdict("requirement") == {
"value": {"simpleValue": "value"},
"@uri": "https://test.com",
"@minOccurs": "0",
"@maxOccurs": "unbounded",
"@cardinality": "required",
"@instructions": "instructions",
}
@@ -1272,11 +1264,11 @@ class TestMaterial:
run("Elements with any material will pass an empty material facet", facet=facet, inst=element, expected=True)
run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True)
facet = Material(minOccurs=0, maxOccurs=0)
facet = Material(cardinality="prohibited")
run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False)
facet = Material(minOccurs=0)
facet = Material(cardinality="optional")
run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True)
facet = Material(value="Foo", minOccurs=0)
facet = Material(value="Foo", cardinality="optional")
run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True)
ifc = ifcopenshell.file()
@@ -1406,23 +1398,24 @@ class TestMaterial:
class TestPartOf:
def test_creating_a_partof_facet(self):
facet = PartOf()
assert facet.asdict() == {"entity": {"name": {"simpleValue": "IFCWALL"}}, "@maxOccurs": "unbounded" }
assert facet.asdict("requirement") == {
"entity": {"name": {"simpleValue": "IFCWALL"}},
"@cardinality": "required",
}
facet = PartOf(
name="IFCGROUP",
predefinedType="predefinedType",
relation="IFCRELASSIGNSTOGROUP",
minOccurs="0",
maxOccurs="unbounded",
cardinality="required",
instructions="instructions",
)
assert facet.asdict() == {
assert facet.asdict("requirement") == {
"entity": {
"name": {"simpleValue": "IFCGROUP"},
"predefinedType": {"simpleValue": "predefinedType"},
},
"@relation": "IFCRELASSIGNSTOGROUP",
"@minOccurs": "0",
"@maxOccurs": "unbounded",
"@cardinality": "required",
"@instructions": "instructions",
}
@@ -1440,11 +1433,8 @@ class TestPartOf:
run("The aggregated part passes an aggregate relationship", facet=facet, inst=subelement, expected=True)
run("A required facet checks all parameters as normal", facet=facet, inst=subelement, expected=True)
facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", minOccurs=0, maxOccurs=0)
facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", cardinality="prohibited")
run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=subelement, expected=False)
facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", minOccurs=0)
run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True)
run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=subelement, expected=True)
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab")
+66 -24
View File
@@ -48,12 +48,11 @@ class TestIds:
def test_create_an_ids_with_minimal_information(self):
specs = ids.Ids()
print('AAA', specs.asdict())
assert specs.asdict() == {
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd",
"info": {"title": "Untitled"},
"specifications": {"specification": []},
}
@@ -73,7 +72,7 @@ class TestIds:
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd",
"info": {
"title": "title",
"copyright": "copyright",
@@ -93,7 +92,7 @@ class TestIds:
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd",
"info": {"title": "Untitled"},
"specifications": {"specification": []},
}
@@ -117,7 +116,7 @@ class TestIds:
specs = ids.Ids(title="Title")
spec = ids.Specification(name="Name")
spec.applicability.append(ids.Entity(name="IFCWALL"))
spec.requirements.append(name_attr := ids.Attribute(name="Name", value="Waldo"))
spec.requirements.append(ids.Attribute(name="Name", value="Waldo"))
specs.specifications.append(spec)
assert "http://standards.buildingsmart.org/IDS" in specs.to_string()
assert spec.status == None
@@ -135,7 +134,7 @@ class TestIds:
specs,
model,
True,
[wall, waldo]
[wall, waldo],
)
spec.ifcVersion = []
@@ -159,10 +158,23 @@ class TestIds:
run("Prohibited specifications fail if at least one entity passes all requirements 1/3", specs, model, True)
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Wally")
run("Prohibited specifications fail if at least one entity passes all requirements 2/3", specs, model, False, [wall], [wall])
run(
"Prohibited specifications fail if at least one entity passes all requirements 2/3",
specs,
model,
False,
[wall],
[wall],
)
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Waldo")
run("Prohibited specifications fail if at least one entity passes all requirements 3/3", specs, model, False, [wall])
run(
"Prohibited specifications fail if at least one entity passes all requirements 3/3",
specs,
model,
False,
[wall],
)
spec.minOccurs = 0
spec.maxOccurs = "unbounded"
@@ -233,12 +245,9 @@ class TestIds:
class TestSpecification:
def test_create_specification_with_minimal_information(self):
spec = ids.Specification()
print(spec.asdict())
assert spec.asdict() == {
"@name": "Unnamed",
"@ifcVersion": ["IFC2X3", "IFC4"],
"@minOccurs": 0,
"@maxOccurs": "unbounded",
"applicability": {},
"requirements": {},
}
@@ -255,8 +264,6 @@ class TestSpecification:
)
assert spec.asdict() == {
"@name": "name",
"@minOccurs": 1,
"@maxOccurs": 1,
"@ifcVersion": "IFC4",
"@identifier": "identifier",
"@description": "description",
@@ -264,43 +271,78 @@ class TestSpecification:
"applicability": {},
"requirements": {},
}
def test_specification_has_no_requirements(self):
model = ifcopenshell.file()
wall = model.createIfcWall()
waldo = model.createIfcWall(Name="Waldo")
test_ids = ids.Ids(title="Title")
spec = ids.Specification(name="Name")
spec.applicability.append(ids.Entity(name="IFCWALL"))
test_ids.specifications.append(spec)
spec.minOccurs = 1
run("A specification that is required and has at least one applicable entity but no requirements shall pass", test_ids, model, True, [wall, waldo], None)
run(
"A specification that is required and has at least one applicable entity but no requirements shall pass",
test_ids,
model,
True,
[wall, waldo],
None,
)
test_ids = ids.Ids(title="Title")
spec = ids.Specification(name="Name")
test_ids.specifications.append(spec)
spec.minOccurs = 1
run("A specification that is required but has no applicable entities or requirements shall fail", test_ids, model, False, None, None)
run(
"A specification that is required but has no applicable entities or requirements shall fail",
test_ids,
model,
False,
None,
None,
)
test_ids = ids.Ids(title="Title")
spec = ids.Specification(name="Name")
spec.applicability.append(ids.Entity(name="IFCWALL"))
test_ids.specifications.append(spec)
spec.minOccurs = 0
run("A specification that is optional and has at least one applicable entity but no requirements shall pass", test_ids, model, True, [wall, waldo], None)
run(
"A specification that is optional and has at least one applicable entity but no requirements shall pass",
test_ids,
model,
True,
[wall, waldo],
None,
)
test_ids = ids.Ids(title="Title")
spec = ids.Specification(name="Name")
spec.applicability.append(ids.Entity(name="IFCWALL"))
test_ids.specifications.append(spec)
spec.minOccurs = 0
spec.maxOccurs = 0
run("A specification that is prohibited and has at least one applicable entity but no requirements shall fail", test_ids, model, False, [wall, waldo], None)
run(
"A specification that is prohibited and has at least one applicable entity but no requirements shall fail",
test_ids,
model,
False,
[wall, waldo],
None,
)
test_ids = ids.Ids(title="Title")
spec = ids.Specification(name="Name")
test_ids.specifications.append(spec)
spec.minOccurs = 0
spec.maxOccurs = 0
run("A specification that is prohibited but has no applicable entities or requirements shall pass", test_ids, model, True, None, None)
run(
"A specification that is prohibited but has no applicable entities or requirements shall pass",
test_ids,
model,
True,
None,
None,
)
+76
View File
@@ -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");
+7
View File
@@ -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>>;
}