mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 04:32:23 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 413886ac6b | |||
| 377f68c020 | |||
| b718a37db2 | |||
| e21043b038 | |||
| 395598d8e3 | |||
| 813518bcb5 | |||
| 4ce7a898c9 | |||
| 78f235ea7c | |||
| bda2f50f9d | |||
| 5e5ba5b3d4 | |||
| 22b0e9dcc5 | |||
| 68420d43ea | |||
| 924e337c90 | |||
| 04599a10d2 | |||
| 84c931bb5f | |||
| 572aedb3eb | |||
| 1db86c24e3 | |||
| 74adde3030 | |||
| f68e8f0e00 | |||
| 98386984f3 | |||
| 7b35ed97ca | |||
| b3302ed679 | |||
| e12f060af8 | |||
| 6d4f47a2ca | |||
| e49ab7e0cc | |||
| ccfe7ab80a | |||
| 7b95358975 | |||
| bcc50aaadc | |||
| 30f63b7d67 | |||
| ead13e23bc | |||
| 24257d2a85 | |||
| 5c4dbb924d | |||
| 88e1b3d2f9 | |||
| f67b80c6a6 | |||
| c7fd42f2cf | |||
| bed03bf83c | |||
| 1e49400e93 | |||
| 608c0dc05d | |||
| 921dbe9dae | |||
| a7e0837f96 | |||
| 071c80f845 | |||
| 06d823b9d8 | |||
| b15e33e5e9 |
@@ -137,7 +137,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9838698-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -23,14 +23,17 @@ import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
from mathutils import Vector
|
||||
from ifcopenshell import util
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim.schema
|
||||
from typing import Optional, Union, Literal
|
||||
|
||||
|
||||
class Qto(blenderbim.core.tool.Qto):
|
||||
@classmethod
|
||||
def get_radius_of_selected_vertices(cls, obj):
|
||||
def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float:
|
||||
selected_verts = [v.co for v in obj.data.vertices if v.select]
|
||||
total = Vector()
|
||||
for v in selected_verts:
|
||||
@@ -39,16 +42,16 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return max([(v - circle_center).length for v in selected_verts])
|
||||
|
||||
@classmethod
|
||||
def set_qto_result(cls, result):
|
||||
def set_qto_result(cls, result: float) -> None:
|
||||
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
|
||||
|
||||
@classmethod
|
||||
def add_object_base_qto(cls, obj):
|
||||
def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
return cls.add_product_base_qto(product)
|
||||
|
||||
@classmethod
|
||||
def add_product_base_qto(cls, product):
|
||||
def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
base_quantity_name = cls.get_applicable_base_quantity_name(product)
|
||||
if base_quantity_name:
|
||||
return tool.Ifc.run(
|
||||
@@ -58,7 +61,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_quantity_names(cls, qto_name):
|
||||
def get_applicable_quantity_names(cls, qto_name: str) -> list[str]:
|
||||
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name)
|
||||
return (
|
||||
[property.Name for property in pset_template.HasPropertyTemplates]
|
||||
@@ -67,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_base_quantity_name(cls, product=None):
|
||||
def get_applicable_base_quantity_name(
|
||||
cls, product: Optional[ifcopenshell.entity_instance] = None
|
||||
) -> Union[str, None]:
|
||||
if not product:
|
||||
return
|
||||
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
|
||||
@@ -76,36 +81,44 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
|
||||
|
||||
@classmethod
|
||||
def get_new_calculated_quantity(cls, qto_name, quantity_name, obj):
|
||||
def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj)
|
||||
|
||||
@classmethod
|
||||
def get_new_guessed_quantity(cls, obj, quantity_name, alternative_prop_names):
|
||||
def get_new_guessed_quantity(
|
||||
cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str]
|
||||
) -> Union[float, None]:
|
||||
return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj)
|
||||
|
||||
@classmethod
|
||||
def get_rounded_value(cls, new_quantity):
|
||||
def get_rounded_value(cls, new_quantity: float) -> float:
|
||||
return round(new_quantity, 3)
|
||||
|
||||
@classmethod
|
||||
def get_calculated_object_quantities(cls, calculator, qto_name, obj):
|
||||
def get_calculated_object_quantities(
|
||||
cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object
|
||||
) -> dict[str, float]:
|
||||
return {
|
||||
quantity_name: cls.get_rounded_value(calculator.calculate_quantity(qto_name, quantity_name, obj))
|
||||
quantity_name: cls.get_rounded_value(value)
|
||||
for quantity_name in cls.get_applicable_quantity_names(qto_name) or []
|
||||
if cls.has_calculator(qto_name, quantity_name)
|
||||
and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None
|
||||
and (value := calculator.calculate_quantity(qto_name, quantity_name, obj)) is not None
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def has_calculator(cls, qto_name, quantity_name):
|
||||
def has_calculator(cls, qto_name: str, quantity_name: str) -> bool:
|
||||
return bool(mapper.get(qto_name, {}).get(quantity_name, None))
|
||||
|
||||
@classmethod
|
||||
def convert_to_project_units(cls, value, qto_name=None, quantity_name=None, quantity_type=None):
|
||||
def convert_to_project_units(
|
||||
cls,
|
||||
value: float,
|
||||
qto_name: Optional[str] = None,
|
||||
quantity_name: Optional[str] = None,
|
||||
quantity_type: Optional[QuanityTypes] = None,
|
||||
) -> Union[float, None]:
|
||||
"""You can either specify `quantity_type` or provide `qto_name/quantity_name`
|
||||
to let method figure the `quantity_type` from the templates
|
||||
|
||||
`quantity_type` values are `Q_LENGTH`, `Q_AREA`, `Q_VOLUME`
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
quantity_to_unit_types = {
|
||||
@@ -135,7 +148,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_guessed_quantities(cls, obj, pset_qto_properties):
|
||||
def get_guessed_quantities(
|
||||
cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance]
|
||||
) -> dict[str, float]:
|
||||
calculated_quantities = {}
|
||||
for pset_qto_property in pset_qto_properties:
|
||||
quantity_name = pset_qto_property.get_info()["Name"]
|
||||
@@ -153,7 +168,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return calculated_quantities
|
||||
|
||||
@classmethod
|
||||
def get_base_qto(cls, product):
|
||||
def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not hasattr(product, "IsDefinedBy"):
|
||||
return
|
||||
for rel in product.IsDefinedBy or []:
|
||||
@@ -166,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
@classmethod
|
||||
def get_related_cost_item_quantities(cls, product):
|
||||
def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]:
|
||||
"""_summary_: Returns the related cost item and related quantities of the product
|
||||
|
||||
:param ifc-instance product: ifc instance
|
||||
|
||||
@@ -21,6 +21,8 @@ import ifcopenshell
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.material
|
||||
import blenderbim.core.style
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
@@ -150,9 +150,9 @@ class Spatial(blenderbim.core.tool.Spatial):
|
||||
for product in products:
|
||||
obj = tool.Ifc.get_object(product)
|
||||
if obj and bpy.context.view_layer.objects.get(obj.name):
|
||||
obj.select_set(True)
|
||||
if unhide:
|
||||
obj.hide_set(False)
|
||||
obj.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def filter_products(cls, products, action):
|
||||
|
||||
@@ -96,7 +96,7 @@ class Style(blenderbim.core.tool.Style):
|
||||
return obj.name
|
||||
|
||||
@classmethod
|
||||
def get_style(cls, obj):
|
||||
def get_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if obj.BIMMaterialProperties.ifc_style_id:
|
||||
try:
|
||||
return tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -107,11 +107,11 @@ Scenario: Add a wall perpendicular to an existing wall
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
When I press "bim.hotkey(hotkey='S_A')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Extend one wall to another
|
||||
Given an empty IFC project
|
||||
@@ -122,35 +122,17 @@ Scenario: Extend one wall to another
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_E')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-2,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-2,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - first wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -162,13 +144,13 @@ Scenario: Join two walls with a butt joint - second wall has priority
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -179,12 +161,30 @@ Scenario: Join two walls with a mitre joint
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Change the height of a wall
|
||||
Given an empty IFC project
|
||||
|
||||
@@ -387,10 +387,10 @@ Scenario: Edit pset length property
|
||||
|
||||
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
|
||||
Scenario: Edit qset length property
|
||||
Given an empty IFC project
|
||||
And I press "mesh.add_clever_stair"
|
||||
@@ -401,15 +401,14 @@ Scenario: Edit qset length property
|
||||
|
||||
# Testing Q_LENGTH type of prop
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57495"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
@@ -24,7 +24,7 @@ Scenario: Unlink object
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material"
|
||||
And I press "bim.add_style()"
|
||||
When I press "bim.unlink_object(obj='IfcWall/Cube')"
|
||||
Then the object "Cube" is not an IFC element
|
||||
And the material "Material" is an IFC style
|
||||
|
||||
@@ -18,7 +18,7 @@ Scenario: Remove style
|
||||
When I press "bim.remove_style(style={style})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Add style
|
||||
Scenario: Add style to current blender material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
@@ -33,32 +33,39 @@ Scenario: Unlink style
|
||||
When I press "bim.unlink_style"
|
||||
Then the material "Material" is not an IFC style
|
||||
|
||||
Scenario: Enable editing style
|
||||
Scenario: Add a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
When I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
When I press "bim.add_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Scenario: Disable adding a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
When I press "bim.disable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
When I press "bim.disable_adding_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Edit style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.edit_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Given an empty IFC project
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.disable_editing_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Load styles
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
|
||||
@@ -51,7 +51,7 @@ def replace_variables(value):
|
||||
|
||||
|
||||
def is_x(number, x):
|
||||
return abs(number - x) < 1e-6
|
||||
return abs(number - x) < 1e-5
|
||||
|
||||
|
||||
def vectors_are_equal(v1, v2):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -135,8 +135,7 @@ class Clasher:
|
||||
self.logger.info(f"Adding objects {name}")
|
||||
assert iterator.initialize()
|
||||
while True:
|
||||
self.tree.add_element(iterator.get_native(), should_triangulate=True)
|
||||
# self.tree.add_element(iterator.get())
|
||||
self.tree.add_element(iterator.get())
|
||||
shape = iterator.get()
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
@@ -28,7 +28,19 @@
|
||||
bool IfcGeom::Kernel::convert(const IfcSchema::IfcDerivedProfileDef* l, TopoDS_Shape& face) {
|
||||
TopoDS_Face f;
|
||||
gp_Trsf2d trsf2d;
|
||||
if (convert_face(l->ParentProfile(), f) && IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
bool is_mirror = false;
|
||||
#ifdef SCHEMA_HAS_IfcMirroredProfileDef
|
||||
if (l->as<IfcSchema::IfcMirroredProfileDef>()) {
|
||||
trsf2d.SetMirror(gp::Origin2d());
|
||||
is_mirror = true;
|
||||
}
|
||||
#endif
|
||||
if (!is_mirror) {
|
||||
if (!IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (convert_face(l->ParentProfile(), f)) {
|
||||
gp_Trsf trsf = trsf2d;
|
||||
face = BRepBuilderAPI_Transform(f, trsf).Shape();
|
||||
return true;
|
||||
|
||||
@@ -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
@@ -0,0 +1,588 @@
|
||||
#include "clash_utils.h"
|
||||
#include <cassert>
|
||||
|
||||
#define GU_CULLING_EPSILON_RAY_TRIANGLE FLT_EPSILON*FLT_EPSILON
|
||||
#define PX_MAX_F32 3.4028234663852885981170418348452e+38F
|
||||
|
||||
typedef uint32_t PxU32;
|
||||
|
||||
// Why can't I use std::clamp?
|
||||
template<typename TC>
|
||||
const TC& ios_clamp(const TC& v, const TC& lo, const TC& hi) {
|
||||
assert(!(hi < lo));
|
||||
return (v < lo) ? lo : (hi < v) ? hi : v;
|
||||
}
|
||||
|
||||
// Branchless slab method. Note that this can still be optimised further by batching boxes.
|
||||
// From Tavian Barnes - MIT License
|
||||
// https://tavianator.com/2022/ray_box_boundary.html
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
|
||||
float tmin = 0.0, tmax = INFINITY;
|
||||
|
||||
for (int d = 0; d < 3; ++d) {
|
||||
bool sign = std::signbit(ray->dir_inv[d]);
|
||||
float bmin = box->corners[sign][d];
|
||||
float bmax = box->corners[!sign][d];
|
||||
|
||||
float dmin = (bmin - ray->origin[d]) * ray->dir_inv[d];
|
||||
float dmax = (bmax - ray->origin[d]) * ray->dir_inv[d];
|
||||
|
||||
tmin = std::max(dmin, tmin);
|
||||
tmax = std::min(dmax, tmax);
|
||||
}
|
||||
|
||||
return tmin < tmax;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge) {
|
||||
// Find vectors for two edges sharing vert0
|
||||
const gp_Vec edge1 = vert1 - vert0;
|
||||
const gp_Vec edge2 = vert2 - vert0;
|
||||
|
||||
// Begin calculating determinant - also used to calculate U parameter
|
||||
const gp_Vec pvec = dir.Crossed(edge2); // error ~ |v2-v0|
|
||||
|
||||
// If determinant is near zero, ray lies in plane of triangle
|
||||
const Standard_Real det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0|
|
||||
|
||||
if(cull)
|
||||
{
|
||||
if(det<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0;
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec);
|
||||
|
||||
const Standard_Real enlargeCoeff = enlarge*det;
|
||||
const Standard_Real uvlimit = -enlargeCoeff;
|
||||
const Standard_Real uvlimit2 = det + enlargeCoeff;
|
||||
|
||||
if(u<uvlimit || u>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec);
|
||||
if(v<uvlimit || (u+v)>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Calculate t, scale parameters, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec);
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
at = t*inv_det;
|
||||
au = u*inv_det;
|
||||
av = v*inv_det;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the non-culling branch
|
||||
if(std::abs(det)<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0; // error ~ |orig-v0|
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec) * inv_det;
|
||||
if(u<-enlarge || u>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec) * inv_det;
|
||||
if(v<-enlarge || (u+v)>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// Calculate t, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec) * inv_det;
|
||||
|
||||
at = t;
|
||||
au = u;
|
||||
av = v;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/sweep/GuSweepCapsuleCapsule.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b) // seg 2 origin, vector
|
||||
{
|
||||
const gp_Vec Tx = q - p;
|
||||
const double ADotA = a.Dot(a);
|
||||
const double BDotB = b.Dot(b);
|
||||
const double ADotB = a.Dot(b);
|
||||
const double ADotT = a.Dot(Tx);
|
||||
const double BDotT = b.Dot(Tx);
|
||||
|
||||
// t parameterizes ray (p, a)
|
||||
// u parameterizes ray (q, b)
|
||||
|
||||
// Compute t for the closest point on ray (p, a) to ray (q, b)
|
||||
const Standard_Real Denom = ADotA*BDotB - ADotB*ADotB;
|
||||
|
||||
Standard_Real t; // We will clamp result so t is on the segment (p, a)
|
||||
if(Denom!=0.0f)
|
||||
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
|
||||
// find u for point on ray (q, b) closest to point at t
|
||||
Standard_Real u;
|
||||
if(BDotB!=0.0f)
|
||||
{
|
||||
u = (t*ADotB - BDotT) / BDotB;
|
||||
|
||||
// if u is on segment (q, b), t and u correspond to closest points, otherwise, clamp u, recompute and clamp t
|
||||
if(u<0.0f)
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
else if(u > 1.0f)
|
||||
{
|
||||
u = 1.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp((ADotB + ADotT) / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
|
||||
x = p + a * t;
|
||||
y = q + b * u;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/distance/GuDistanceTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q)
|
||||
{
|
||||
std::array<gp_Vec, 3> Sv;
|
||||
Sv[0] = p[1] - p[0];
|
||||
Sv[1] = p[2] - p[1];
|
||||
Sv[2] = p[0] - p[2];
|
||||
|
||||
std::array<gp_Vec, 3> Tv;
|
||||
Tv[0] = q[1] - q[0];
|
||||
Tv[1] = q[2] - q[1];
|
||||
Tv[2] = q[0] - q[2];
|
||||
|
||||
gp_Vec minP, minQ;
|
||||
bool shown_disjoint = false;
|
||||
|
||||
float mindd = PX_MAX_F32;
|
||||
|
||||
for(int i=0;i<3;i++)
|
||||
{
|
||||
for(int j=0;j<3;j++)
|
||||
{
|
||||
edgeEdgeDist(cp, cq, p[i], Sv[i], q[j], Tv[j]);
|
||||
const gp_Vec V = cq - cp;
|
||||
const float dd = V.Dot(V);
|
||||
|
||||
if(dd<=mindd)
|
||||
{
|
||||
minP = cp;
|
||||
minQ = cq;
|
||||
mindd = dd;
|
||||
|
||||
int id = i+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
gp_Vec Z = p[id] - cp;
|
||||
float a = Z.Dot(V);
|
||||
id = j+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
Z = q[id] - cq;
|
||||
float b = Z.Dot(V);
|
||||
|
||||
if((a<=0.0f) && (b>=0.0f))
|
||||
return V.Dot(V);
|
||||
|
||||
if(a<=0.0f) a = 0.0f;
|
||||
else if(b>0.0f) b = 0.0f;
|
||||
|
||||
if((mindd - a + b) > 0.0f)
|
||||
shown_disjoint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Sn = Sv[0].Crossed(Sv[1]);
|
||||
float Snl = Sn.Dot(Sn);
|
||||
|
||||
if(Snl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Tp = {(p[0] - q[0]).Dot(Sn),
|
||||
(p[0] - q[1]).Dot(Sn),
|
||||
(p[0] - q[2]).Dot(Sn)};
|
||||
|
||||
int index = -1;
|
||||
if((Tp[0]>0.0f) && (Tp[1]>0.0f) && (Tp[2]>0.0f))
|
||||
{
|
||||
if(Tp[0]<Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]<Tp[index]) index = 2;
|
||||
}
|
||||
else if((Tp[0]<0.0f) && (Tp[1]<0.0f) && (Tp[2]<0.0f))
|
||||
{
|
||||
if(Tp[0]>Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]>Tp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& qIndex = q[index];
|
||||
|
||||
gp_Vec V = qIndex - p[0];
|
||||
gp_Vec Z = Sn.Crossed(Sv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[1];
|
||||
Z = Sn.Crossed(Sv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[2];
|
||||
Z = Sn.Crossed(Sv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = qIndex + Sn * Tp[index]/Snl;
|
||||
cq = qIndex;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
|
||||
float Tnl = Tn.Dot(Tn);
|
||||
|
||||
if(Tnl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Sp = {(q[0] - p[0]).Dot(Tn),
|
||||
(q[0] - p[1]).Dot(Tn),
|
||||
(q[0] - p[2]).Dot(Tn)};
|
||||
|
||||
int index = -1;
|
||||
if((Sp[0]>0.0f) && (Sp[1]>0.0f) && (Sp[2]>0.0f))
|
||||
{
|
||||
if(Sp[0]<Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]<Sp[index]) index = 2;
|
||||
}
|
||||
else if((Sp[0]<0.0f) && (Sp[1]<0.0f) && (Sp[2]<0.0f))
|
||||
{
|
||||
if(Sp[0]>Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]>Sp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& pIndex = p[index];
|
||||
|
||||
gp_Vec V = pIndex - q[0];
|
||||
gp_Vec Z = Tn.Crossed(Tv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[1];
|
||||
Z = Tn.Crossed(Tv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[2];
|
||||
Z = Tn.Crossed(Tv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = pIndex;
|
||||
cq = pIndex + Tn * Sp[index]/Tnl;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(shown_disjoint)
|
||||
{
|
||||
cp = minP;
|
||||
cq = minQ;
|
||||
return mindd;
|
||||
}
|
||||
else return 0.0f;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
//Based on the paper A Fast Triangle-Triangle Intersection Test by T. Moeller
|
||||
//http://web.stanford.edu/class/cs277/resources/papers/Moller1997b.pdf
|
||||
namespace {
|
||||
struct Interval
|
||||
{
|
||||
Standard_Real min;
|
||||
Standard_Real max;
|
||||
gp_Vec minPoint;
|
||||
gp_Vec maxPoint;
|
||||
|
||||
Interval() : min(FLT_MAX), max(-FLT_MAX), minPoint(gp_Vec(NAN, NAN, NAN)), maxPoint(gp_Vec(NAN, NAN, NAN)) { }
|
||||
|
||||
static bool overlapOrTouch(const Interval& a, const Interval& b)
|
||||
{
|
||||
return !(a.min > b.max || b.min > a.max);
|
||||
}
|
||||
|
||||
static Interval intersection(const Interval& a, const Interval& b)
|
||||
{
|
||||
Interval result;
|
||||
if (!overlapOrTouch(a, b))
|
||||
return result;
|
||||
|
||||
if (a.min > b.min) {
|
||||
result.min = a.min;
|
||||
result.minPoint = a.minPoint;
|
||||
} else {
|
||||
result.min = b.min;
|
||||
result.minPoint = b.minPoint;
|
||||
}
|
||||
|
||||
if (a.max < b.max) {
|
||||
result.max = a.max;
|
||||
result.maxPoint = a.maxPoint;
|
||||
} else {
|
||||
result.max = b.max;
|
||||
result.maxPoint = b.maxPoint;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void include(Standard_Real d, const gp_Vec& p)
|
||||
{
|
||||
if (d < min) { min = d; minPoint = p; }
|
||||
if (d > max) { max = d; maxPoint = p; }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB, Standard_Real distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir)
|
||||
{
|
||||
Interval i;
|
||||
|
||||
const bool bA = distanceA > 0;
|
||||
const bool bB = distanceB > 0;
|
||||
const bool bC = distanceC > 0;
|
||||
distanceA = std::abs(distanceA);
|
||||
distanceB = std::abs(distanceB);
|
||||
distanceC = std::abs(distanceC);
|
||||
|
||||
if (bA != bB)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceB)) * b + (distanceB / (distanceA + distanceB)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bA != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceC)) * c + (distanceC / (distanceA + distanceC)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bB != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceB / (distanceB + distanceC)) * c + (distanceC / (distanceB + distanceC)) * b;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y)
|
||||
{
|
||||
return (a.Coord(y) - c.Coord(y)) * (b.Coord(x) - c.Coord(x)) - (a.Coord(x) - c.Coord(x)) * (b.Coord(y) - c.Coord(y));
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real ab = orient2d(a, b, point, x, y);
|
||||
const Standard_Real bc = orient2d(b, c, point, x, y);
|
||||
const Standard_Real ca = orient2d(c, a, point, x, y);
|
||||
|
||||
if ((ab >= 0) == (bc >= 0) && (ab >= 0) == (ca >= 0))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real aaS = orient2d(startA, endA, startB, x, y);
|
||||
const Standard_Real aaE = orient2d(startA, endA, endB, x, y);
|
||||
|
||||
if ((aaS >= 0) == (aaE >= 0))
|
||||
return false;
|
||||
|
||||
const Standard_Real bbS = orient2d(startB, endB, startA, x, y);
|
||||
const Standard_Real bbE = orient2d(startB, endB, endA, x, y);
|
||||
|
||||
if ((bbS >= 0) == (bbE >= 0))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void getProjectionIndices(gp_Vec normal, PxU32& x, PxU32& y)
|
||||
{
|
||||
normal.SetCoord(std::abs(normal.X()), std::abs(normal.Y()), std::abs(normal.Z()));
|
||||
|
||||
if (normal.X() >= normal.Y() && normal.X() >= normal.Z())
|
||||
{
|
||||
//x is the dominant normal direction
|
||||
x = 1;
|
||||
y = 2;
|
||||
}
|
||||
else if (normal.Y() >= normal.X() && normal.Y() >= normal.Z())
|
||||
{
|
||||
//y is the dominant normal direction
|
||||
x = 2;
|
||||
y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
//z is the dominant normal direction
|
||||
x = 0;
|
||||
y = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2)
|
||||
{
|
||||
PxU32 x = 0;
|
||||
PxU32 y = 0;
|
||||
getProjectionIndices(p1_n, x, y);
|
||||
|
||||
const Standard_Real third = (1.0f / 3.0f);
|
||||
|
||||
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
|
||||
//difference is not very big and the coplanar case is not expected to be the most common case
|
||||
if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) ||
|
||||
linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) ||
|
||||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
|
||||
pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// Also with minor modification to return intersection points.
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar)
|
||||
{
|
||||
const Standard_Real tolerance = 1e-8f;
|
||||
|
||||
gp_Vec p1_n((b1 - a1).Crossed(c1 - a1).Normalized());
|
||||
double p1_d = -a1.Dot(p1_n);
|
||||
// const PxPlane p1(a1, b1, c1);
|
||||
const Standard_Real p1ToA = a2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToB = b2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToC = c2.Dot(p1_n) + p1_d;
|
||||
|
||||
if(std::abs(p1ToA) < tolerance && std::abs(p1ToB) < tolerance &&std::abs(p1ToC) < tolerance)
|
||||
return ignoreCoplanar ? false : trianglesIntersectCoplanar(p1_n, a1, b1, c1, a2, b2, c2); //Coplanar triangles
|
||||
|
||||
if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0))
|
||||
return false; //All points of triangle 2 on same side of triangle 1 -> no intersection
|
||||
|
||||
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
|
||||
double p2_d = -a2.Dot(p2_n);
|
||||
// const PxPlane p2(a2, b2, c2);
|
||||
const Standard_Real p2ToA = a1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToB = b1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToC = c1.Dot(p2_n) + p2_d;
|
||||
|
||||
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
|
||||
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
|
||||
|
||||
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
|
||||
const Standard_Real l2 = intersectionDirection.SquareMagnitude();
|
||||
intersectionDirection *= 1.0f / std::sqrt(l2);
|
||||
|
||||
const Interval i1 = computeInterval(p2ToA, p2ToB, p2ToC, a1, b1, c1, intersectionDirection);
|
||||
const Interval i2 = computeInterval(p1ToA, p1ToB, p1ToC, a2, b2, c2, intersectionDirection);
|
||||
|
||||
if (Interval::overlapOrTouch(i1, i2))
|
||||
{
|
||||
/*if (intersection)
|
||||
{
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
intersection->p0 = i.minPoint;
|
||||
intersection->p1 = i.maxPoint;
|
||||
}*/
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
int1 = i.minPoint;
|
||||
int2 = i.maxPoint;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <gp_Vec.hxx>
|
||||
#include <array>
|
||||
|
||||
struct ray {
|
||||
float origin[3];
|
||||
float dir[3];
|
||||
float dir_inv[3];
|
||||
};
|
||||
|
||||
struct box {
|
||||
float corners[2][3];
|
||||
};
|
||||
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box);
|
||||
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge=0.0f);
|
||||
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b); // seg 2 origin, vector
|
||||
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q);
|
||||
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar);
|
||||
@@ -109,7 +109,7 @@ endif
|
||||
mkdir -p dist/ifcopenshell
|
||||
cp -r ifcopenshell/* dist/ifcopenshell/
|
||||
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9838698-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/
|
||||
ifeq ($(PLATFORM), win)
|
||||
|
||||
@@ -20,11 +20,11 @@ Pre-built packages
|
||||
| build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ |
|
||||
+----------------+----------------+----------------+----------------+------------------+
|
||||
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9838698-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and run IfcConvert using the command line.
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
Geometry tree
|
||||
=============
|
||||
|
||||
IfcOpenShell includes a utility to build a unbalanced binary tree of geometry
|
||||
and their bounding boxes. After a tree is built, you can efficiently select
|
||||
geometry by specifying a point, radius, or bounding box.
|
||||
IfcOpenShell includes a utility to build trees of geometry and their bounding
|
||||
boxes. Geometry trees can be used to efficiently select geometry or collide
|
||||
geometry with one another.
|
||||
|
||||
.. image:: images/geometry-tree.png
|
||||
|
||||
The most efficient way to build tree is by using the iterator, as shown in the
|
||||
example below:
|
||||
The most efficient way to build a tree is by using the iterator. If the native
|
||||
OpenCASCADE shape is added to the tree, a **UB Tree** is built. Alternatively,
|
||||
if triangulation is added to the tree, a **BVH Tree** is built. The type of
|
||||
tree determines the type of operation you can perform.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -21,27 +23,208 @@ example below:
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
# Use triangulation to build a BVH tree
|
||||
tree.add_element(iterator.get())
|
||||
|
||||
# Alternatively, use this code to build an unbalanced binary tree
|
||||
# tree.add_element(iterator.get_native())
|
||||
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
Once built, there are three methods you can use to select elements in the tree:
|
||||
``select_box``, ``select``, and ``select_ray``.
|
||||
Clashing or selecting geometry from a geometry tree
|
||||
---------------------------------------------------
|
||||
|
||||
``select_box`` lets you query for elements that contain a point or another
|
||||
element. However, it only checks the bounding box of elements instead of their
|
||||
exact geometry. This is the fastest approach and is recommended if you don't
|
||||
need precise geometry selection.
|
||||
With a **BVH Tree**, you can efficiently clash sets of elements with other
|
||||
elements. You can find elements that intersect, collide, or are within a
|
||||
clearance distance threshold of one another. There are three methods you can
|
||||
use to clash elements in the tree. Each function collides one set of elements
|
||||
with another set of elements.
|
||||
|
||||
``select`` lets you query for elements that contain a point, a sphere, or
|
||||
another element. ``select`` is similar to select box, but additionally
|
||||
considers the actual geometry of the object. This is slower but more precise.
|
||||
- `Detecting intersection clashes between elements`_ detects when an element intersects with another
|
||||
element. This is the most common type of clash detection used when
|
||||
coordinating designs. For example, you might want to know if any pipes go
|
||||
through structural columns or beams.
|
||||
- `Detecting collision clashes between elements`_ detects when an element
|
||||
touches another element. It is the fastest type of clash detection but does
|
||||
not consider the distance that an element goes inside another element. This
|
||||
considers surfaces only so it works on non-manifold geometry but will not
|
||||
detect if an element is completely within another element.
|
||||
- `Detecting clearance clashes between elements`_ detects when an element comes
|
||||
near to another element within a clearance threshold. This is the slowest
|
||||
type of clash detection. It works on non-manifold geometry and does not
|
||||
consider inside vs outside. Elements like pipe and ducts with insulation,
|
||||
structural openings, and equipment will typically require clearance checks.
|
||||
|
||||
``select_ray`` lets you query for elements that intersect with a ray.
|
||||
With a **UB Tree**, you can efficiently select geometry by specifying a point,
|
||||
radius, or bounding box. There are three methods you can use to select elements
|
||||
in the tree.
|
||||
|
||||
- `Selecting elements using bounding boxes`_ lets you query for elements that
|
||||
contain a point or another element. However, it only checks the bounding box
|
||||
of elements instead of their exact geometry. This is the fastest approach and
|
||||
is recommended if you don't need precise geometry selection.
|
||||
- `Selecting elements using precise geometry`_ lets you query for elements that
|
||||
contain a point, a sphere, or another element. This is similar to selecting
|
||||
using bounding boxes, but additionally considers the actual geometry of the
|
||||
element. This is slower but more precise.
|
||||
- `Selecting elements using a ray`_ lets you query for elements that intersect
|
||||
with a ray.
|
||||
|
||||
Detecting intersection clashes between elements
|
||||
-----------------------------------------------
|
||||
|
||||
``clash_intersection_many`` detects when an element intersects with or is
|
||||
contained within another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_intersection_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
tolerance=0.002, # Any protrusions less than 2mm are ignored
|
||||
check_all=True, # Keep on checking all potential intersections to find a worst case protrusion distance
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# Potential clash types that can be detected are protrusions, pierces, and collisions
|
||||
clash_type = ["protrusion", "pierce", "collision", "clearance"][clash.clash_type],
|
||||
|
||||
# P1 and P2 represents two XYZ coordinates. The meaning of the coordinate depends on the clash type.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the protrusion or piercing distance in meters.
|
||||
# It is also the distance between P1 and P2.
|
||||
distance = clash.distance
|
||||
|
||||
If you specify a ``tolerance`` value, intersections with a protrusion distance
|
||||
smaller than this tolerance are excluded. It is recommended to specify a
|
||||
non-zero tolerance to distinguish between when elements merely touch (e.g. a
|
||||
GPO on a wall) versus if they are truly intersecting (e.g. a pipe going through
|
||||
a beam).
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as an
|
||||
intersection is found. This is faster but may not return the worst-case
|
||||
protrusion distance. If you are not interested in the protrusion distance, it
|
||||
is recommended to set this to ``False``. If you want the protrusion distance,
|
||||
such as to prioritise which clashes are more severe, set this to ``True``.
|
||||
|
||||
This includes:
|
||||
|
||||
1. When an element X protrudes inside element Y, where element Y is manifold.
|
||||
In this case, a protrusion distance is calculated as the deepest point of
|
||||
element X to the closest surface of element Y. ``P1`` is defined as the XYZ
|
||||
coordinate on element X, and ``P2`` is defined as the nearest point on the
|
||||
surface of element Y.
|
||||
2. When an element X pierces element Y, such that an edge of element X enters
|
||||
element Y and leaves through another face. In this case, a piercing distance
|
||||
is calculated as the distance where that edge is inside element Y. ``P1`` is
|
||||
defined as the point on an edge of element X which enters element Y, and
|
||||
``P2`` is the point where that edge leaves element Y.
|
||||
3. When neither X or Y is manifold, we cannot detect protrusion or piercing, so
|
||||
instead when X and Y have any touching face. This is the same as the
|
||||
``clash_collision_many`` check below. The distance is considered to be zero
|
||||
and ignores your specified tolerance. ``P1`` and ``P2`` are equal and
|
||||
represent an arbitrary XYZ point where the two elements touch.
|
||||
|
||||
Detecting collision clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_collision_many`` detects when the surface of an element collides with
|
||||
another element. The surfaces may either merely touch (e.g. are coplanar) or
|
||||
intersect.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_collision_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
allow_touching=True, # Include results where faces merely touch but do not intersect
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents two possible arbitrary points where a collision is found.
|
||||
# P1 may or may not be equal to P2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
A collision between two surface triangles may be "touching" or "intersecting".
|
||||
Two touching triangles may be coplanar or merely have a single edge or vertex
|
||||
touching the other triangle. An intersecting triangle will have at least one
|
||||
edge that goes through the other triangle.
|
||||
|
||||
Detecting clearance clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_clearance_many`` detects with the surface of an element comes within a
|
||||
clearance distance threshold of another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_clearance_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
clearance=0.1, # Any surface closer than than 100mm is a clash
|
||||
check_all=False, # Stop measuring distances once the first clearance violation is found per element.
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents the two XYZ coordinates between element1 and element2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the distance between element1 and element2 that is less than the clearance.
|
||||
# It is the distance between P1 and P2. It cannot be less than 0.
|
||||
distance = clash.distance
|
||||
|
||||
You cannot specify a ``clearance`` less than 0.
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as a
|
||||
clearance violation is found. This is faster but may not return the worst-case
|
||||
distance. If you only interested whether there is a clearance issue, it is
|
||||
recommended to set this to ``False``. If you want the exact worst case
|
||||
clearance distance, such as to prioritise which clashes are more severe, set
|
||||
this to ``True``.
|
||||
|
||||
Selecting elements using bounding boxes
|
||||
---------------------------------------
|
||||
|
||||
Elements may be queried using an axis aligned bounding box. An axis aligned
|
||||
bounding box is the bounding box using global XYZ axes, not the element's local
|
||||
XYZ axes. If you have a vertical construction project, this means that your
|
||||
model should be oriented to project north to get the best results.
|
||||
|
||||
You may select all elements that have a bounding box containing the point with
|
||||
XYZ coordinates of ``(0., 0., 0.)``.
|
||||
|
||||
@@ -128,11 +311,11 @@ geometry. It will return:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
elements = tree.select_box(wall, completely_within=True)
|
||||
elements = tree.select(wall, completely_within=True)
|
||||
|
||||
# Alternatively, you may also specify an extension to dilate the geometry
|
||||
# of the wall.
|
||||
elements = tree.select_box(wall, completely_within=True, extend=5.)
|
||||
elements = tree.select(wall, completely_within=True, extend=5.)
|
||||
|
||||
Selecting elements using a ray
|
||||
------------------------------
|
||||
|
||||
@@ -30,10 +30,6 @@ the API.
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| | Linux 64bit | Windows 32bit | Windows 64bit | MacOS Intel 64bit | MacOS Silicon 64bit |
|
||||
+=============+================+================+================+===================+=====================+
|
||||
| Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ |
|
||||
@@ -43,36 +39,26 @@ the API.
|
||||
| Python 3.12 | py312-linux64_ | py312-win32_ | py312-win64_ | py312-macos64_ | py312-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
|
||||
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win32.zip
|
||||
.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win32.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win32.zip
|
||||
.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win64.zip
|
||||
.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win64.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win64.zip
|
||||
.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-linux64.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-win32.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-win64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-macos64.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9838698-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9838698-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9838698-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-9838698-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
|
||||
Python path. If you're not sure where your Python path is, run the following
|
||||
|
||||
@@ -61,7 +61,8 @@ operating systems. GCC (4.7 or newer) or Clang (any version) is required.
|
||||
|
||||
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
|
||||
|
||||
3. Install OpenCascade Technology (OCCT).
|
||||
3. Install OpenCascade Technology (OCCT). Officially v7.5.0 is supported. Other
|
||||
versions may have unexpected behaviour.
|
||||
|
||||
::
|
||||
|
||||
|
||||
@@ -195,6 +195,18 @@ class tree(ifcopenshell_wrapper.tree):
|
||||
args.append(kwargs.get("extend", -1.0e-5))
|
||||
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)]
|
||||
|
||||
def clash_intersection_many(self, set_a, set_b, tolerance=0.002, check_all=True):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], tolerance, check_all]
|
||||
return ifcopenshell_wrapper.tree.clash_intersection_many(*args)
|
||||
|
||||
def clash_collision_many(self, set_a, set_b, allow_touching=False):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], allow_touching]
|
||||
return ifcopenshell_wrapper.tree.clash_collision_many(*args)
|
||||
|
||||
def clash_clearance_many(self, set_a, set_b, clearance=0.05, check_all=False):
|
||||
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], clearance, check_all]
|
||||
return ifcopenshell_wrapper.tree.clash_clearance_many(*args)
|
||||
|
||||
|
||||
def create_shape(
|
||||
settings: settings, inst: entity_instance, repr: Optional[entity_instance] = None
|
||||
|
||||
@@ -179,12 +179,14 @@ def get_property_definition(
|
||||
if not definition:
|
||||
return
|
||||
|
||||
ifc_class = definition.is_a()
|
||||
|
||||
if prop:
|
||||
if definition.is_a("IfcElementQuantity"):
|
||||
if ifc_class == "IfcElementQuantity":
|
||||
return get_quantity(definition.Quantities, prop, verbose=verbose)
|
||||
elif definition.is_a("IfcPropertySet"):
|
||||
elif ifc_class == "IfcPropertySet":
|
||||
return get_property(definition.HasProperties, prop, verbose=verbose)
|
||||
elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"):
|
||||
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
||||
return get_property(definition.Properties, prop, verbose=verbose)
|
||||
else:
|
||||
# Entity introduced in IFC4
|
||||
@@ -196,12 +198,12 @@ def get_property_definition(
|
||||
return
|
||||
|
||||
props = {}
|
||||
if definition.is_a("IfcElementQuantity"):
|
||||
props.update(get_quantities(definition.Quantities, verbose=verbose))
|
||||
elif definition.is_a("IfcPropertySet"):
|
||||
props.update(get_properties(definition.HasProperties, verbose=verbose))
|
||||
elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"):
|
||||
props.update(get_properties(definition.Properties, verbose=verbose))
|
||||
if ifc_class == "IfcElementQuantity":
|
||||
props.update(get_quantities(definition[5], verbose=verbose))
|
||||
elif ifc_class == "IfcPropertySet":
|
||||
props.update(get_properties(definition[4], verbose=verbose))
|
||||
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
||||
props.update(get_properties(definition[2], verbose=verbose))
|
||||
else:
|
||||
# Entity introduced in IFC4
|
||||
# definition.is_a('IfcPreDefinedPropertySet'):
|
||||
@@ -216,7 +218,7 @@ def get_quantity(
|
||||
quantities: list[ifcopenshell.entity_instance], name: str, verbose=False
|
||||
) -> Union[Any, dict[str, Any]]:
|
||||
for quantity in quantities or []:
|
||||
if quantity.Name != name:
|
||||
if quantity[0] != name:
|
||||
continue
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
result = quantity[3]
|
||||
@@ -234,23 +236,23 @@ def get_quantities(quantities: list[ifcopenshell.entity_instance], verbose=False
|
||||
results = {}
|
||||
for quantity in quantities or []:
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
results[quantity.Name] = quantity[3]
|
||||
results[quantity[0]] = quantity[3]
|
||||
if verbose:
|
||||
results[quantity.Name] = {
|
||||
results[quantity[0]] = {
|
||||
"id": quantity.id(),
|
||||
"class": quantity.is_a(),
|
||||
"value": results[quantity.Name],
|
||||
"value": results[quantity[0]],
|
||||
}
|
||||
elif quantity.is_a("IfcPhysicalComplexQuantity"):
|
||||
data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"}
|
||||
data["properties"] = get_quantities(quantity.HasQuantities)
|
||||
del data["HasQuantities"]
|
||||
results[quantity.Name] = data
|
||||
results[quantity[0]] = data
|
||||
if verbose:
|
||||
results[quantity.Name] = {
|
||||
results[quantity[0]] = {
|
||||
"id": quantity.id(),
|
||||
"class": quantity.is_a(),
|
||||
"value": results[quantity.Name],
|
||||
"value": results[quantity[0]],
|
||||
}
|
||||
return results
|
||||
|
||||
@@ -262,7 +264,7 @@ def get_property(
|
||||
if prop.Name != name:
|
||||
continue
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
result = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
||||
result = prop[2].wrappedValue if prop[2] else None
|
||||
elif prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
result = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
elif prop.is_a("IfcPropertyListValue"):
|
||||
@@ -286,35 +288,36 @@ def get_property(
|
||||
def get_properties(properties: list[ifcopenshell.entity_instance], verbose=False) -> dict[str, dict[str, Any]]:
|
||||
results = {}
|
||||
for prop in properties or []:
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
results[prop.Name] = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
||||
ifc_class = prop.is_a()
|
||||
if ifc_class == "IfcPropertySingleValue":
|
||||
results[prop[0]] = prop[2].wrappedValue if prop[2] else None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
results[prop.Name] = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyEnumeratedValue":
|
||||
results[prop[0]] = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyListValue"):
|
||||
results[prop.Name] = [v.wrappedValue for v in prop.ListValues] or None
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyListValue":
|
||||
results[prop[0]] = [v.wrappedValue for v in prop.ListValues] or None
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyBoundedValue"):
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyBoundedValue":
|
||||
data = prop.get_info()
|
||||
del data["Unit"]
|
||||
results[prop.Name] = data
|
||||
results[prop[0]] = data
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcPropertyTableValue"):
|
||||
results[prop.Name] = prop.get_info()
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcPropertyTableValue":
|
||||
results[prop[0]] = prop.get_info()
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
elif prop.is_a("IfcComplexProperty"):
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
elif ifc_class == "IfcComplexProperty":
|
||||
data = {k: v for k, v in prop.get_info().items() if v is not None and k != "Name"}
|
||||
data["properties"] = get_properties(prop.HasProperties)
|
||||
del data["HasProperties"]
|
||||
results[prop.Name] = data
|
||||
results[prop[0]] = data
|
||||
if verbose:
|
||||
results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]}
|
||||
results[prop[0]] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop[0]]}
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import pathlib
|
||||
import ifcopenshell
|
||||
@@ -25,10 +26,10 @@ from ifcopenshell.entity_instance import entity_instance
|
||||
from functools import lru_cache
|
||||
from typing import List, Generator, Optional
|
||||
|
||||
templates = {}
|
||||
templates: dict[str, PsetQto] = {}
|
||||
|
||||
|
||||
def get_template(schema):
|
||||
def get_template(schema: str) -> PsetQto:
|
||||
global templates
|
||||
if schema not in templates:
|
||||
templates[schema] = PsetQto(schema)
|
||||
@@ -36,11 +37,13 @@ def get_template(schema):
|
||||
|
||||
|
||||
class PsetQto:
|
||||
# fmt: off
|
||||
templates_path = {
|
||||
"IFC2X3": "Pset_IFC2X3.ifc",
|
||||
"IFC4": "Pset_IFC4_ADD2.ifc",
|
||||
"IFC4X3": "Pset_IFC4X3.ifc"
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, schema: str, templates=None) -> None:
|
||||
self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from fractions import Fraction
|
||||
from math import pi
|
||||
from typing import Tuple, Iterable, Any, Union, Literal
|
||||
from typing import Tuple, Iterable, Any, Union, Literal, Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
@@ -525,7 +525,7 @@ def convert_unit(value, from_unit, to_unit):
|
||||
)
|
||||
|
||||
|
||||
def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
def convert(value: float, from_prefix: Optional[str], from_unit: str, to_prefix: Optional[str], to_unit: str) -> float:
|
||||
"""Converts between length, area, and volume units
|
||||
|
||||
In this case, you manually specify the names and (optionally) prefixes to
|
||||
@@ -534,12 +534,12 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
|
||||
:param value: The numeric value you want to convert
|
||||
:type value: float
|
||||
:param from_prefix: A prefix from IfcSIPrefix. Can be None.
|
||||
:type from_prefix: str,optional
|
||||
:param from_prefix: A prefix from IfcSIPrefix. Can be None
|
||||
:type from_prefix: str, optional
|
||||
:param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
|
||||
:type from_unit: str
|
||||
:param to_prefix: A prefix from IfcSIPrefix. Can be None.
|
||||
:type to_prefix: str,optional
|
||||
:param to_prefix: A prefix from IfcSIPrefix. Can be None
|
||||
:type to_prefix: str, optional
|
||||
:param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
|
||||
:type to_unit: str
|
||||
:return: The converted value.
|
||||
@@ -566,7 +566,7 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
|
||||
return value
|
||||
|
||||
|
||||
def calculate_unit_scale(ifc_file, unit_type="LENGTHUNIT"):
|
||||
def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUNIT") -> float:
|
||||
"""Returns a unit scale factor to convert to and from IFC project units and SI units.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -16,7 +16,7 @@ classifiers = [
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
geometry = ["mathutils", "shapely"]
|
||||
date = ["isodate"]
|
||||
date = ["isodate", "dateutil"]
|
||||
[project.urls]
|
||||
"Homepage" = "http://ifcopenshell.org"
|
||||
"Bug Tracker" = "https://github.com/ifcopenshell/ifcopenshell/issues"
|
||||
|
||||
@@ -740,6 +740,8 @@ size_t IfcParse::IfcFile::load(unsigned entity_instance_name, const IfcParse::en
|
||||
filler.push_back(ea);
|
||||
} catch (IfcException& e) {
|
||||
Logger::Message(Logger::LOG_ERROR, e.what());
|
||||
// #4070 We didn't actually capture an aggregate entry, undo length increment.
|
||||
return_value--;
|
||||
}
|
||||
} else {
|
||||
filler.push_back(new TokenArgument(next));
|
||||
|
||||
@@ -52,8 +52,8 @@ class Patcher:
|
||||
self.query = query
|
||||
|
||||
def patch(self):
|
||||
self.contained_ins = {}
|
||||
self.aggregates = {}
|
||||
self.contained_ins: dict[str, set[ifcopenshell.entity_instance]] = {}
|
||||
self.aggregates: dict[str, set[ifcopenshell.entity_instance]] = {}
|
||||
self.new = ifcopenshell.file(schema=self.file.wrapped_data.schema)
|
||||
self.owner_history = None
|
||||
self.reuse_identities: dict[int, ifcopenshell.entity_instance] = {}
|
||||
@@ -66,18 +66,14 @@ class Patcher:
|
||||
self.create_spatial_tree()
|
||||
self.file = self.new
|
||||
|
||||
def add_element(self, element) -> None:
|
||||
def add_element(self, element: ifcopenshell.entity_instance) -> None:
|
||||
new_element = self.append_asset(element)
|
||||
if not new_element:
|
||||
return
|
||||
for rel in getattr(element, "ContainedInStructure", []):
|
||||
spatial_element = rel.RelatingStructure
|
||||
new_spatial_element = self.append_asset(spatial_element)
|
||||
self.contained_ins.setdefault(spatial_element.GlobalId, set()).add(new_element)
|
||||
self.add_decomposition_parents(spatial_element, new_spatial_element)
|
||||
self.add_spatial_structures(element, new_element)
|
||||
self.add_decomposition_parents(element, new_element)
|
||||
|
||||
def append_asset(self, element) -> Union[ifcopenshell.entity_instance, None]:
|
||||
def append_asset(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
try:
|
||||
return self.new.by_guid(element.GlobalId)
|
||||
except:
|
||||
@@ -88,29 +84,43 @@ class Patcher:
|
||||
"project.append_asset", self.new, library=self.file, element=element, reuse_identities=self.reuse_identities
|
||||
)
|
||||
|
||||
def add_decomposition_parents(self, element, new_element) -> None:
|
||||
def add_spatial_structures(
|
||||
self, element: ifcopenshell.entity_instance, new_element: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
"""element is IfcElement"""
|
||||
for rel in getattr(element, "ContainedInStructure", []):
|
||||
spatial_element = rel.RelatingStructure
|
||||
new_spatial_element = self.append_asset(spatial_element)
|
||||
self.contained_ins.setdefault(spatial_element.GlobalId, set()).add(new_element)
|
||||
self.add_decomposition_parents(spatial_element, new_spatial_element)
|
||||
|
||||
def add_decomposition_parents(
|
||||
self, element: ifcopenshell.entity_instance, new_element: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
"""element is IfcObjectDefinition"""
|
||||
for rel in element.Decomposes:
|
||||
parent = rel.RelatingObject
|
||||
new_parent = self.append_asset(parent)
|
||||
self.aggregates.setdefault(parent.GlobalId, set()).add(new_element)
|
||||
self.add_decomposition_parents(parent, new_parent)
|
||||
self.add_spatial_structures(parent, new_parent)
|
||||
|
||||
def create_spatial_tree(self) -> None:
|
||||
for relating_structure, related_elements in self.contained_ins.items():
|
||||
for relating_structure_guid, related_elements in self.contained_ins.items():
|
||||
self.new.createIfcRelContainedInSpatialStructure(
|
||||
ifcopenshell.guid.new(),
|
||||
self.owner_history,
|
||||
None,
|
||||
None,
|
||||
list(related_elements),
|
||||
self.new.by_guid(relating_structure),
|
||||
self.new.by_guid(relating_structure_guid),
|
||||
)
|
||||
for relating_object, related_objects in self.aggregates.items():
|
||||
for relating_object_guid, related_objects in self.aggregates.items():
|
||||
self.new.createIfcRelAggregates(
|
||||
ifcopenshell.guid.new(),
|
||||
self.owner_history,
|
||||
None,
|
||||
None,
|
||||
self.new.by_guid(relating_object),
|
||||
self.new.by_guid(relating_object_guid),
|
||||
list(related_objects),
|
||||
)
|
||||
|
||||
@@ -20,11 +20,59 @@ import os
|
||||
import pytest
|
||||
import ifcpatch
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
class TestExtractElements:
|
||||
def test_basic(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
|
||||
wall = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
|
||||
output = ifcpatch.execute({"file": ifc_file, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
|
||||
assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId
|
||||
assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId
|
||||
|
||||
def test_keep_spatial_structure(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
|
||||
|
||||
site = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcSite")
|
||||
building = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcBuilding")
|
||||
storey = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcBuildingStorey")
|
||||
wall = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.run("aggregate.assign_object", ifc_file, product=building, relating_object=site)
|
||||
ifcopenshell.api.run("aggregate.assign_object", ifc_file, product=storey, relating_object=building)
|
||||
ifcopenshell.api.run("spatial.assign_container", ifc_file, product=wall, relating_structure=storey)
|
||||
|
||||
output = ifcpatch.execute({"file": ifc_file, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
|
||||
wall_new = output.by_type("IfcWall")[0]
|
||||
assert (storey_new := ifcopenshell.util.element.get_container(wall_new)).GlobalId == storey.GlobalId
|
||||
assert (building_new := ifcopenshell.util.element.get_aggregate(storey_new)).GlobalId == building.GlobalId
|
||||
assert (site_new := ifcopenshell.util.element.get_aggregate(building_new)).GlobalId == site.GlobalId
|
||||
|
||||
def test_keep_aggregate_in_spatial_structure(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
|
||||
|
||||
element = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcElementAssembly")
|
||||
container = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcBuildingStorey")
|
||||
subelement = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.run("spatial.assign_container", ifc_file, product=element, relating_structure=container)
|
||||
ifcopenshell.api.run("aggregate.assign_object", ifc_file, product=subelement, relating_object=element)
|
||||
|
||||
output = ifcpatch.execute({"file": ifc_file, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
|
||||
wall_new = output.by_type("IfcWall")[0]
|
||||
assembly = output.by_type("IfcElementAssembly")[0]
|
||||
|
||||
assert ifcopenshell.util.element.get_aggregate(wall_new).GlobalId == element.GlobalId
|
||||
assert ifcopenshell.util.element.get_container(assembly).GlobalId == container.GlobalId
|
||||
|
||||
def test_getting_the_psets_of_a_product_as_a_dictionary(self):
|
||||
ifc = ifcopenshell.open(os.path.join(os.getcwd(), "test", "files", "basic.ifc"))
|
||||
output = ifcpatch.execute({"input": ifc, "recipe": "ExtractElements", "arguments": [".IfcWall"]})
|
||||
output = ifcpatch.execute({"file": ifc, "recipe": "ExtractElements", "arguments": ["IfcWall"]})
|
||||
assert output.by_type("IfcWall")
|
||||
assert not output.by_type("IfcSlab")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -82,6 +82,8 @@ std::pair<char const*, size_t> vector_to_buffer(const T& t) {
|
||||
|
||||
%template(ray_intersection_results) std::vector<IfcGeom::ray_intersection_result>;
|
||||
|
||||
%template(clashes) std::vector<IfcGeom::clash>;
|
||||
|
||||
// A Template instantantation should be defined before it is used as a base class.
|
||||
// But frankly I don't care as most methods are subtlely different anyway.
|
||||
%include "../ifcgeom_schema_agnostic/IfcGeomTree.h"
|
||||
@@ -114,6 +116,80 @@ std::pair<char const*, size_t> vector_to_buffer(const T& t) {
|
||||
return IfcGeom_tree_vector_to_list(ps);
|
||||
}
|
||||
|
||||
|
||||
%typemap(in) const std::vector<IfcUtil::IfcBaseClass*>& (std::vector<IfcUtil::IfcBaseClass*> temp) {
|
||||
if (!PyList_Check($input)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Expected a list.");
|
||||
return NULL;
|
||||
}
|
||||
$1 = &temp; // Set $1 to the address of temp, which SWIG will use as the argument in the wrapped function
|
||||
temp.reserve(PyList_Size($input)); // Pre-allocate memory for efficiency
|
||||
for (Py_ssize_t i = 0; i < PyList_Size($input); ++i) {
|
||||
PyObject* pyObj = PyList_GetItem($input, i);
|
||||
void* ptr = 0;
|
||||
int res = SWIG_ConvertPtr(pyObj, &ptr, SWIGTYPE_p_IfcUtil__IfcBaseClass, 0);
|
||||
if (!SWIG_IsOK(res)) {
|
||||
PyErr_SetString(PyExc_TypeError, "List item is not of type IfcBaseClass.");
|
||||
return NULL;
|
||||
}
|
||||
temp.push_back(reinterpret_cast<IfcUtil::IfcBaseClass*>(ptr));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<clash> clash_intersection_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, double tolerance, bool check_all) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_intersection_many(set_a_entities, set_b_entities, tolerance, check_all);
|
||||
}
|
||||
|
||||
std::vector<clash> clash_collision_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, bool allow_touching) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_collision_many(set_a_entities, set_b_entities, allow_touching);
|
||||
}
|
||||
|
||||
std::vector<clash> clash_clearance_many(const std::vector<IfcUtil::IfcBaseClass*>& set_a, const std::vector<IfcUtil::IfcBaseClass*>& set_b, double clearance, bool check_all) const {
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_a_entities;
|
||||
std::vector<IfcUtil::IfcBaseEntity*> set_b_entities;
|
||||
for (auto* e : set_a) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_a_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
for (auto* e : set_b) {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("All instances should be of type IfcProduct");
|
||||
}
|
||||
set_b_entities.push_back(static_cast<IfcUtil::IfcBaseEntity*>(e));
|
||||
}
|
||||
return $self->clash_clearance_many(set_a_entities, set_b_entities, clearance, check_all);
|
||||
}
|
||||
|
||||
aggregate_of_instance::ptr select(IfcUtil::IfcBaseClass* e, bool completely_within = false, double extend = 0.0) const {
|
||||
if (!e->declaration().is("IfcProduct")) {
|
||||
throw IfcParse::IfcException("Instance should be an IfcProduct");
|
||||
|
||||
@@ -203,7 +203,14 @@
|
||||
|
||||
%include "IfcGeomWrapper.i"
|
||||
%include "IfcParseWrapper.i"
|
||||
%include "std_vector.i"
|
||||
|
||||
namespace std {
|
||||
%template(float_array_3) array<double, 3>;
|
||||
%template(FloatVector) vector<float>;
|
||||
%template(IntVector) std::vector<int>;
|
||||
%template(DoubleVector) std::vector<double>;
|
||||
%template(StringVector) std::vector<std::string>;
|
||||
%template(FloatVectorVector) std::vector<std::vector<float>>;
|
||||
%template(DoubleVectorVector) std::vector<std::vector<double>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user