Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0

This commit is contained in:
Carlos Villagrasa
2022-07-22 18:42:25 +02:00
committed by GitHub
36 changed files with 475 additions and 71 deletions
+9
View File
@@ -132,6 +132,15 @@ def get_enum_items(data, prop_name, context):
return items
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
x, y = event.mouse_x, event.mouse_y
bpy.context.window.cursor_warp(10, 10)
move_back = lambda: bpy.context.window.cursor_warp(x, y)
bpy.app.timers.register(move_back, first_interval=0.01)
class IfcHeaderExtractor:
def __init__(self, filepath: str):
self.filepath = filepath
@@ -31,6 +31,7 @@ classes = (
operator.EnableEditingGroup,
operator.DisableEditingGroup,
operator.SelectGroupProducts,
operator.UpdateGroup,
prop.Group,
prop.BIMGroupProperties,
ui.BIM_PT_groups,
@@ -22,6 +22,7 @@ import ifcopenshell.api
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.group.data import Data
from ifcopenshell.util.selector import Selector
class LoadGroups(bpy.types.Operator):
@@ -36,6 +37,7 @@ class LoadGroups(bpy.types.Operator):
new = props.groups.add()
new.ifc_definition_id = ifc_definition_id
new.name = group["Name"]
new.selection_query = group["Description"].split("*selector*")[1] if group["Description"] else ""
props.is_editing = True
bpy.ops.bim.disable_editing_group()
return {"FINISHED"}
@@ -167,7 +169,7 @@ class AssignGroup(bpy.types.Operator):
"group.assign_group",
self.file,
**{
"product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
"product": [self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
"group": self.file.by_id(self.group),
}
)
@@ -219,3 +221,31 @@ class SelectGroupProducts(bpy.types.Operator):
if self.group in product_groups:
obj.select_set(True)
return {"FINISHED"}
class UpdateGroup(bpy.types.Operator):
bl_idname = "bim.update_group"
bl_label = "Update Group"
bl_options = {"REGISTER", "UNDO"}
query: bpy.props.StringProperty()
group_id: bpy.props.IntProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
group = self.file.by_id(self.group_id)
query = self.query
new_products = Selector.parse(self.file, query)
ifcopenshell.api.run(
"group.update_group_products",
self.file,
**{
"products": new_products,
"group": group,
}
)
Data.load(IfcStore.get_file())
bpy.ops.bim.load_groups()
return {"FINISHED"}
@@ -34,7 +34,8 @@ from bpy.props import (
class Group(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
selection_query: StringProperty(name="Selection Query")
class BIMGroupProperties(PropertyGroup):
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
@@ -40,7 +40,7 @@ class BIM_PT_groups(Panel):
self.props = context.scene.BIMGroupProperties
row = self.layout.row(align=True)
row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER")
row.label(text=f"{len(Data.groups)} Groups Found", icon="OUTLINER")
if self.props.is_editing:
row.operator("bim.add_group", text="", icon="ADD")
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
@@ -118,7 +118,7 @@ class BIM_UL_groups(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
row.label(text=f"*{item.name}") if item.selection_query != "" else row.label(text=item.name)
group_id = item.ifc_definition_id
if context.scene.BIMGroupProperties.active_group_id == group_id:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
@@ -130,6 +130,10 @@ class BIM_UL_groups(UIList):
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
if item.selection_query != "":
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
op.group_id = item.ifc_definition_id
op.query = item.selection_query
else:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = group_id
@@ -137,12 +141,18 @@ class BIM_UL_groups(UIList):
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
if item.selection_query != "":
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
op.group_id = item.ifc_definition_id
op.query = item.selection_query
class BIM_UL_object_groups(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
row.label(text=f"*{item.name}") if item.selection_query != "" else row.label(text=item.name)
op = row.operator("bim.remove_group", text="", icon="X")
op.group = item.ifc_definition_id
op = row.operator("bim.assign_group", text="", icon="ADD")
op.group = item.ifc_definition_id
@@ -315,12 +315,16 @@ class AppendLibraryElement(bpy.types.Operator):
bl_idname = "bim.append_library_element"
bl_label = "Append Library Element"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Append element to the current project"
definition: bpy.props.IntProperty()
prop_index: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return IfcStore.get_file()
poll = bool(IfcStore.get_file())
if bpy.app.version > (3, 0, 0) and not poll:
cls.poll_message_set("Please create or load a project first.")
return poll
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
@@ -38,6 +38,7 @@ classes = (
operator.SaveSelectorQuery,
operator.OpenQueryLibrary,
operator.LoadQuery,
operator.AddToIfcGroup,
prop.BIMFilterClasses,
prop.BIMFilterBuildingStoreys,
prop.BIMSearchProperties,
@@ -20,9 +20,11 @@ import re
import bpy
import ifcopenshell
import ifcopenshell.util.element
from ifcopenshell.api.group.data import Data
from ifcopenshell.util.selector import Selector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import close_operator_panel
from itertools import cycle
from bpy.types import PropertyGroup, Operator
from bpy.props import (
@@ -462,7 +464,6 @@ class ActivateIfcBuildingStoreyFilter(Operator):
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT"
class UnhideAllElements(Operator):
"""Filter model elements based on selection"""
@@ -479,7 +480,6 @@ class UnhideAllElements(Operator):
class FilterModelElements(Operator):
"""Filter model elements based on selection"""
bl_idname = "bim.filter_model_elements"
bl_label = "Filter Model Elements"
option: StringProperty("select|isolate|hide")
@@ -513,7 +513,7 @@ class FilterModelElements(Operator):
selection = self.add_filters(selection, query)
elif query.selector == "GlobalId":
selection += f"#{query.global_id}"
selection += f"#{query.value}"
elif query.selector == "IfcElementType":
index = int(query.active_sub_option.split(":")[0])
@@ -562,7 +562,6 @@ class FilterModelElements(Operator):
class IfcSelector(Operator):
"""Select elements in model with IFC Selector"""
bl_idname = "bim.ifc_selector"
bl_label = "Select elements with IFC Selector"
@@ -603,19 +602,11 @@ class SaveSelectorQuery(Operator):
class OpenQueryLibrary(Operator):
"""Open Query Library"""
bl_idname = "bim.open_query_library"
bl_label = "Open Query Library"
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
def close_panel(event):
x, y = event.mouse_x, event.mouse_y
bpy.context.window.cursor_warp(10, 10)
move_back = lambda: bpy.context.window.cursor_warp(x, y)
bpy.app.timers.register(move_back, first_interval=0.001)
return context.window_manager.invoke_popup(self, width=400)
def draw(self, context):
layout = self.layout
@@ -641,8 +632,37 @@ class LoadQuery(Operator):
bl_idname = "bim.load_query"
bl_label = "Load Query"
index: IntProperty()
def invoke(self, context, event):
close_operator_panel(event)
return self.execute(context)
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
return {"FINISHED"}
class AddToIfcGroup(Operator):
bl_idname = "bim.add_to_ifc_group"
bl_label = "Add to IFC Group"
group_name: StringProperty(name="Group Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
layout = self.layout
layout.prop(self, "group_name")
def execute(self, context):
self.file = IfcStore.get_file()
ifc_selector = context.scene.IfcSelectorProperties
selector_query_syntax = ifc_selector.selector_query_syntax
group = ifcopenshell.api.run("group.add_group", self.file, **{"Name": self.group_name, "Description": f'*selector*{selector_query_syntax}*selector*'})
objects = Selector.parse(self.file, selector_query_syntax)
ifcopenshell.api.run("group.assign_group", self.file, **{"product": objects, "group": group})
Data.load(IfcStore.get_file())
return {"FINISHED"}
@@ -153,7 +153,8 @@ class IfcSelectorUI:
row = layout.row(align=True)
row.alignment = "CENTER"
row.operator("bim.save_selector_query", text="Save Query")
row.operator("bim.open_query_library", text="Load Query")
op = row.operator("bim.open_query_library", text="Load Query")
row.operator("bim.add_to_ifc_group", text="Add to IFC Group")
def draw_query_group_ui(self, ifc_selector, layout):
for index, group in enumerate(ifc_selector.groups):
@@ -662,7 +662,7 @@ class AddStructuralLoadGroup(bpy.types.Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
load_group = ifcopenshell.api.run("structural.add_structural_load_group", self.file)
ifcopenshell.api.run("group.assign_group", self.file, product=load_group, group=self.file.by_id(self.load_case))
ifcopenshell.api.run("group.assign_group", self.file, product=[load_group], group=self.file.by_id(self.load_case))
Data.load(IfcStore.get_file())
return {"FINISHED"}
@@ -754,7 +754,7 @@ class AddStructuralActivity(bpy.types.Operator):
structural_member=element,
)
ifcopenshell.api.run(
"group.assign_group", self.file, product=activity, group=self.file.by_id(self.load_group)
"group.assign_group", self.file, product=[activity], group=self.file.by_id(self.load_group)
)
Data.load(IfcStore.get_file())
bpy.ops.bim.enable_editing_structural_load_group_activities(load_group=self.load_group)
+8 -2
View File
@@ -27,7 +27,7 @@ from . import schema
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.ui import IFCFileSelector
from blenderbim.bim.helper import get_enum_items
from blenderbim.bim.helper import get_enum_items, close_operator_panel
from mathutils import Vector, Matrix, Euler
from math import radians
@@ -531,6 +531,7 @@ def update_enum_property_search_prop(self, context):
for i, prop in enumerate(self.collection_names):
if prop.name == self.dummy_name:
setattr(context.data, self.prop_name, self.collection_identifiers[i].name)
close_operator_panel(self)
break
@@ -542,8 +543,11 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
collection_names: bpy.props.CollectionProperty(type=StrProperty)
collection_identifiers: bpy.props.CollectionProperty(type=StrProperty)
prop_name: bpy.props.StringProperty()
mouse_x: bpy.props.IntProperty()
mouse_y: bpy.props.IntProperty()
def invoke(self, context, event):
self.mouse_x, self.mouse_y = event.mouse_x, event.mouse_y
self.clear_collections()
self.data = context.data
items = get_enum_items(self.data, self.prop_name, context)
@@ -551,7 +555,9 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
return {"FINISHED"}
self.add_items_regular(items)
self.add_items_suggestions()
return context.window_manager.invoke_props_dialog(self)
# Cursor is moved when we update dummy_name, set it back
context.window.cursor_warp(self.mouse_x, self.mouse_y)
return context.window_manager.invoke_props_popup(self, event)
def draw(self, context):
# Mandatory to access context.data in update :
+3 -3
View File
@@ -148,7 +148,7 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
)
group = ifc.run("group.add_group")
ifc.run("group.edit_group", group=group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
ifc.run("group.assign_group", group=group, product=element)
ifc.run("group.assign_group", group=group, product=[element])
collector.assign(camera)
pset = ifc.run("pset.add_pset", product=element, name="EPset_Drawing")
ifc.run(
@@ -203,7 +203,7 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
context=context,
ifc_representation_class=drawing_tool.get_ifc_representation_class(object_type),
)
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), product=element)
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), product=[element])
collector.assign(obj)
drawing_tool.enable_editing(obj)
@@ -251,7 +251,7 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
if annotation:
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
ifc.run("group.assign_group", group=group, product=annotation)
ifc.run("group.assign_group", group=group, product=[annotation])
collector.assign(ifc.get_object(annotation))
if reference_obj and ifc.is_moved(reference_obj):
+14 -3
View File
@@ -6,7 +6,8 @@ Running tests
The BlenderBIM Add-on has three layers of tests for each of its three technology
layers. These roughly form a test pyramid, moving from many abstract domain
logic tests, to low-level concrete unit tests, to a minimal number of UI and
smoke tests. These tests use ``pytest`` as the test framework and runner.
smoke tests. These tests use ``pytest`` as the test framework and runner, so you
will need to install ``pytest``.
All development is expected to use test driven development, and so we expect
test coverage to be 100% where it is technically possible to test.
@@ -39,8 +40,8 @@ similar.
Tool tests
----------
The tool layer tests actual concrete functions. These have the following
dependencies:
The tool layer tests actual concrete functions. You will need to install the
following dependencies:
* pytest-blender, accessible to your system's Python
* Blender executable, accessible to pytest-blender on your system's Python
@@ -51,6 +52,16 @@ dependencies:
You can install the dependencies by running the ``scripts/setup_pytest.py``
script in Blender.
.. warning::
The ``scripts/setup_pytest.py`` may not work for all operating systems and
installation environments. In this case, you may be required to install the
dependencies manually.
Please be aware that some Blender may come packaged with its own Python,
which may be separate to the Python installation on your system. Be sure to
install the dependencies to the correct Python environment.
Then, run the tests. This will launch Blender headlessly and check the behaviour
of all concrete functions.
+32 -8
View File
@@ -16,16 +16,40 @@
# 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 subprocess
import sys
from pathlib import Path
import subprocess
print("Here are the detected system paths:")
print(sys.path)
py_exec = str(sys.executable)
lib = Path(py_exec).parent.parent / "lib"
print("Detected executable:", py_exec)
subprocess.call([py_exec, "-m", "ensurepip", "--user"])
subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pytest"])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pytest-blender"])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pytest-bdd"])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pygments"])
subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
sys_paths = [p for p in sys.path if "site-packages" in p]
if sys_paths:
print("Detected installation directory:", sys_paths[-1])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest"])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-bdd"])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-blender"])
subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pygments"])
else:
print("Could not detect installation directory. Good luck.")
subprocess.call([py_exec, "-m", "pip", "install", "pytest"])
subprocess.call([py_exec, "-m", "pip", "install", "pytest-bdd"])
subprocess.call([py_exec, "-m", "pip", "install", "pytest-blender"])
subprocess.call([py_exec, "-m", "pip", "install", "pygments"])
try:
import pytest
import pytest_bdd
import pytest_blender
import pygments
print("Test dependency installation was successful!")
except Exception as e:
print("Installation failed :(")
print(e)
@@ -0,0 +1,17 @@
@search
Feature: Search
Scenario: Select all walls
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a new group to IfcSelector
And I add a new query to IfcSelector
And I set "scene.IfcSelectorProperties.groups[0].queries[0].selector" to "IFC Class"
And I set "scene.IfcSelectorProperties.groups[0].queries[0].active_option" to "124: IfcWall"
When I press "bim.filter_model_elements(option='select')"
Then the object "IfcWall/Cube" is selected
+9
View File
@@ -99,6 +99,15 @@ def i_add_a_sun():
def i_add_a_material():
bpy.context.active_object.active_material = bpy.data.materials.new("Material")
@given("I add a new group to IfcSelector")
@when("I add a new group to IfcSelector")
def i_add_a_new_collection_item():
bpy.data.scenes["Scene"].IfcSelectorProperties.groups.add()
@given("I add a new query to IfcSelector")
@when("I add a new query to IfcSelector")
def i_add_a_new_collection_item():
bpy.data.scenes["Scene"].IfcSelectorProperties.groups[0].queries.add()
@given(parsers.parse('the material "{name}" colour is set to "{colour}"'))
@when(parsers.parse('the material "{name}" colour is set to "{colour}"'))
+2 -2
View File
@@ -223,7 +223,7 @@ class TestAddDrawing:
ifc.run(
"group.edit_group", group="group", attributes={"Name": "name", "ObjectType": "DRAWING"}
).should_be_called()
ifc.run("group.assign_group", group="group", product="element").should_be_called()
ifc.run("group.assign_group", group="group", product=["element"]).should_be_called()
collector.assign("obj").should_be_called()
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
ifc.run(
@@ -293,7 +293,7 @@ class TestAddAnnotation:
ifc_representation_class="ifc_representation_class",
).should_be_called().will_return("element")
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
ifc.run("group.assign_group", group="group", product="element").should_be_called()
ifc.run("group.assign_group", group="group", product=["element"]).should_be_called()
collector.assign("obj").should_be_called()
drawing.enable_editing("obj").should_be_called()
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
+2 -2
View File
@@ -222,7 +222,7 @@ class TestAssign(NewFile):
tool.Ifc.link(element, element_obj)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
group.ObjectType = "DRAWING"
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=element, group=group)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=[element], group=group)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
assert bpy.data.collections.get("Views").children.get("IfcGroup/Unnamed")
@@ -235,7 +235,7 @@ class TestAssign(NewFile):
tool.Ifc.link(element, element_obj)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
group.ObjectType = "DRAWING"
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=element, group=group)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=[element], group=group)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
assert bpy.data.collections.get("Views").children.get("IfcGroup/Unnamed")
+2 -2
View File
@@ -260,7 +260,7 @@ class TestGetDrawingGroup(NewFile):
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
group = ifcopenshell.api.run("group.add_group", ifc)
ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
assert subject.get_drawing_group(element) == group
@@ -280,7 +280,7 @@ class TestGetGroupElements(NewFile):
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
group = ifcopenshell.api.run("group.add_group", ifc)
ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
assert subject.get_group_elements(group) == (element,)
@@ -23,7 +23,10 @@ import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {}
self.settings = {
"Name": "Unnamed",
"Description": "",
}
for key, value in settings.items():
self.settings[key] = value
@@ -33,6 +36,7 @@ class Usecase:
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"Name": "Unnamed",
"Name": self.settings["Name"],
"Description": self.settings["Description"],
}
)
@@ -37,12 +37,13 @@ class Usecase:
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["product"]],
"RelatedObjects": self.settings["product"],
"RelatingGroup": self.settings["group"],
}
)
rel = self.settings["group"].IsGroupedBy[0]
related_objects = set(rel.RelatedObjects) or set()
related_objects.add(self.settings["product"])
for obj in self.settings["product"]:
related_objects.add(obj)
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
@@ -20,7 +20,8 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"group": None, "attributes": {}}
self.settings = {
"group": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
@@ -0,0 +1,33 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {
"group": None,
"products": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
rel = self.settings["group"].IsGroupedBy[0]
rel.RelatedObjects = self.settings["products"]
@@ -41,6 +41,8 @@ class Usecase:
properties = self.settings["pset"].HasProperties or []
elif self.settings["pset"].is_a("IfcQuantitySet"):
properties = self.settings["pset"].Quantities or []
elif self.settings["pset"].is_a() in ("IfcMaterialProperties", "IfcProfileProperties"):
properties = self.settings["pset"].Properties or []
for prop in properties:
self.file.remove(prop)
self.file.remove(self.settings["pset"])
@@ -189,7 +189,7 @@ class Selector:
value = None
for element in elements:
element_value = cls.get_element_value(element, key)
if element_value is None and value is not None:
if element_value is None and value is not None and "not" not in comparison:
continue
if comparison and cls.filter_element(element, element_value, comparison, value):
results.append(element)
@@ -29,6 +29,22 @@ class TestRemovePset(test.bootstrap.IFC4):
assert len(self.file.by_type("IfcRelDefinesByProperties")) == 0
assert len(self.file.by_type("IfcPropertySet")) == 0
def test_removing_material_psets(self):
element = self.file.createIfcMaterial()
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar")
assert len(element.HasProperties) == 1
ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset)
assert len(element.HasProperties) == 0
assert len(self.file.by_type("IfcMaterialProperties")) == 0
def test_removing_profile_psets(self):
element = self.file.createIfcProfileDef()
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar")
assert len(element.HasProperties) == 1
ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset)
assert len(element.HasProperties) == 0
assert len(self.file.by_type("IfcMaterialProperties")) == 0
def test_only_unassigning_if_pset_is_used_by_other_elements(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+2 -2
View File
@@ -1111,7 +1111,7 @@ class TestIdsAuthoring(unittest.TestCase):
group = ifcopenshell.api.run("group.add_group", ifc)
facet = ids.partOf.create(entity="IfcGroup")
run("", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcGroup can be passed by subtypes
@@ -1119,7 +1119,7 @@ class TestIdsAuthoring(unittest.TestCase):
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifc.createIfcInventory()
facet = ids.partOf.create(entity="IfcGroup")
ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcSystem only checks that a system is assigned without any other logic
@@ -112,6 +112,27 @@ class TestSelector(test.bootstrap.IFC4):
assert subject.Selector.parse(self.file, '.IfcElement[Name*="oba"]') == [element]
assert subject.Selector.parse(self.file, '.IfcElement[Name*="abc"]') == []
def test_selecting_if_value_not_matching(self):
element_1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element_2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
pset_1 = ifcopenshell.api.run("pset.add_pset", self.file, product=element_1, name="Foo_Bar")
pset_2 = ifcopenshell.api.run("pset.add_pset", self.file, product=element_2, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset_1, properties={"Foo": "Bar"})
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset_2, properties={"Foo": "BOO"})
assert subject.Selector.parse(self.file, '.IfcElement[Foo_Bar.Foo != "Bar"]') == [element_2]
assert subject.Selector.parse(self.file, '.IfcElement[Foo_Bar.Foo != "BOO"]') == [element_1]
def test_selecting_when_attribute_is_none(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
assert subject.Selector.parse(self.file, '.IfcElement[PredefinedType !="non-existent predefined type"]') == [element]
def test_selecting_a_property_which_includes_non_standard_characters(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="a !%$§&/()?|*-+,€~#@µ^°a")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"a !%$§&/()?|*-+,€~#@µ^°a": "Bar"})
assert subject.Selector.parse(self.file, '.IfcElement[a !%$§&/()?|*-+,€~#@µ^°a.a !%$§&/()?|*-+,€~#@µ^°a="Bar"]') == [element]
def test_comparing_if_value_is_in_a_list(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element.Name = "Foobar"
+1
View File
@@ -42,6 +42,7 @@ def cast_to_value(from_value, to_value):
class Facet:
def __init__(self, *parameters):
self.status = None
self.failed_entities = []
self.failed_reasons = []
for i, name in enumerate(self.parameters):
+4 -1
View File
@@ -217,7 +217,8 @@ class Specification:
self.applicable_entities.append(element)
for facet in self.requirements:
result = facet(element)
if not bool(result):
facet.status = bool(result)
if not facet.status:
self.failed_entities.add(element)
facet.failed_entities.append(element)
facet.failed_reasons.append(str(result))
@@ -227,5 +228,7 @@ class Specification:
self.status = False
elif self.minOccurs != 0 and not self.applicable_entities:
self.status = False
for facet in self.requirements:
facet.status = False
elif len(self.applicable_entities) > (self.maxOccurs or 1):
self.status = False
+3 -3
View File
@@ -28,10 +28,10 @@ class Reporter:
def report(self, ids):
pass
def to_string():
def to_string(self):
return ""
def write(filepath):
def write(self, filepath):
pass
@@ -142,7 +142,7 @@ class Json(Reporter):
requirements.append(
{
"description": requirement.to_string("requirement"),
"success": not requirement.failed_entities,
"status": requirement.status,
"failed_entities": [
{"reason": requirement.failed_reasons[i], "element": str(e)}
for i, e in enumerate(requirement.failed_entities[0:10])
+2 -2
View File
@@ -1157,7 +1157,7 @@ class TestPartOf:
group = ifcopenshell.api.run("group.add_group", ifc)
facet = PartOf(entity="IfcGroup")
run("", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcGroup can be passed by subtypes
@@ -1165,7 +1165,7 @@ class TestPartOf:
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifc.createIfcInventory()
facet = PartOf(entity="IfcGroup")
ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcSystem only checks that a system is assigned without any other logic
+77
View File
@@ -0,0 +1,77 @@
# IfcTester - IDS based model auditing
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcTester.
#
# IfcTester is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcTester 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
import os
import time
import ifctester
import ifctester.reporter
import ifcopenshell
from flask import Flask, request, send_from_directory
app = Flask(__name__)
class Ifc:
ifc = None
filepath = None
@classmethod
def get(cls, filepath=None):
if filepath is None or filepath == cls.filepath:
return cls.ifc
cls.filepath = filepath
cls.ifc = ifcopenshell.open(filepath)
return cls.ifc
@app.route("/")
def index():
with open("www/index.html") as template:
return template.read()
@app.route("/<path:asset>.<string:ext>")
def get_asset(asset, ext):
if ext in ("js", "css"):
return send_from_directory("www", asset + "." + ext)
@app.route("/audit", methods=["POST"])
def audit():
filename = ifcopenshell.guid.new()
ids_filepath = os.path.join("uploads", filename + ".ids")
ifc_filepath = os.path.join("uploads", filename + ".ifc")
request.files.get("ids").save(ids_filepath)
request.files.get("ifc").save(ifc_filepath)
start = time.time()
specs = ifctester.open(ids_filepath)
ifc = Ifc.get(ifc_filepath)
print("Finished loading:", time.time() - start)
start = time.time()
specs.validate(ifc)
print("Finished validating:", time.time() - start)
start = time.time()
os.remove(ids_filepath)
os.remove(ifc_filepath)
engine = ifctester.reporter.Json(specs)
engine.report()
return engine.to_string()
+85 -3
View File
@@ -10,6 +10,7 @@ class IDSContainer extends HTMLElement {
this.filename = 'specifications.ids';
this.ids = null;
this.containerId = crypto.randomUUID();
this.isEditing = true;
}
}
@@ -315,6 +316,13 @@ class IDSFacets extends HTMLElement {
}
feather.replace();
}
showResults(requirements) {
var facetElements = this.getElementsByTagName('ids-facet');
for (var i=0; i<facetElements.length; i++) {
facetElements[i].showResults(requirements[i]);
}
}
}
class IDSFacetInstructions extends HTMLElement {
@@ -401,6 +409,21 @@ class IDSFacet extends HTMLElement {
}
}
showResults(requirement) {
var idsResultElements = this.parentElement.getElementsByTagName('ids-result');
for (var i=0; i<idsResultElements.length; i++) {
if (! idsResultElements[i].classList.contains('hidden')) {
idsResultElements[i].classList.add('hidden');
}
if (requirement.status == true && idsResultElements[i].attributes['name'].value == 'pass') {
idsResultElements[i].classList.remove('hidden');
} else if (requirement.status == false && idsResultElements[i].attributes['name'].value == 'fail') {
idsResultElements[i].classList.remove('hidden');
idsResultElements[i].getElementsByTagName('span')[0].textContent = requirement.failed_entities.length;
}
}
}
renderTemplate(templates, parameters) {
for (var i=0; i<templates.length; i++) {
var hasKeys = true;
@@ -747,6 +770,13 @@ class IDSSpecs extends HTMLElement {
}
feather.replace();
}
showResults(specifications) {
var specElements = this.getElementsByTagName('ids-spec');
for (var i=0; i<specElements.length; i++) {
specElements[i].showResults(specifications[i]);
}
}
}
class IDSSpec extends HTMLElement {
@@ -786,6 +816,15 @@ class IDSSpec extends HTMLElement {
}
}
showResults(specification) {
var facetsElements = this.getElementsByTagName('ids-facets');
for (var i=0; i<facetsElements.length; i++) {
if (facetsElements[i].attributes['name'].value == "requirements") {
facetsElements[i].showResults(specification.requirements);
}
}
}
dragover(e) {
// The HTML draggable API is terrible.
e.preventDefault();
@@ -809,7 +848,7 @@ class IDSLoader extends HTMLElement {
inputElement.accept = '.ids,.xml';
inputElement.multiple = false;
inputElement.addEventListener("change", this.loadFile)
inputElement.dispatchEvent(new MouseEvent("click"));
inputElement.dispatchEvent(new MouseEvent("click"));
}
loadFile(e) {
@@ -845,9 +884,8 @@ class IDSSave extends HTMLElement {
}
click() {
var xmlString = new XMLSerializer().serializeToString(this.closest('ids-container').ids);
console.log(xmlString);
var container = this.closest('ids-container')
var xmlString = new XMLSerializer().serializeToString(container.ids);
this.download(container.filename, xmlString);
}
@@ -862,9 +900,53 @@ class IDSSave extends HTMLElement {
}
}
class IDSAudit extends HTMLElement {
connectedCallback() {
this.addEventListener('click', this.launchFileBrowser);
}
launchFileBrowser(accept, callback) {
var inputElement = document.createElement("input");
inputElement.idsAudit = this;
inputElement.type = "file";
inputElement.accept = '.ifc';
inputElement.multiple = false;
inputElement.addEventListener("change", this.loadFile)
inputElement.dispatchEvent(new MouseEvent("click"));
}
loadFile(e) {
var self = this.idsAudit;
var container = self.closest('ids-container');
var request = new XMLHttpRequest();
request.onreadystatechange = function() { self.processResponse(request); };
request.open("POST", "audit");
var data = new FormData();
data.append('ifc', this.files[0]);
data.append('ids', new Blob([new XMLSerializer().serializeToString(container.ids)], {type:'text/plain'}));
request.send(data);
}
processResponse(request) {
if (request.readyState != 4) {
return;
}
var container = this.closest('ids-container');
container.isEditing = false;
var results = JSON.parse(request.responseText);
var specsElements = container.getElementsByTagName('ids-specs');
for (var i=0; i<specsElements.length; i++) {
var specs = specsElements[i];
specs.showResults(results.specifications);
}
}
}
window.customElements.define('ids-container', IDSContainer);
window.customElements.define('ids-loader', IDSLoader);
window.customElements.define('ids-save', IDSSave);
window.customElements.define('ids-audit', IDSAudit);
window.customElements.define('ids-info', IDSInfo);
window.customElements.define('ids-info-element', IDSInfoElement);
window.customElements.define('ids-specs', IDSSpecs);
+13 -4
View File
@@ -47,9 +47,11 @@
<i data-feather="download"></i> SAVE
</a>
</ids-save>
<a href="#">
<i data-feather="play"></i> AUDIT MODEL
</a>
<ids-audit>
<a href="#">
<i data-feather="play"></i> AUDIT MODEL
</a>
</ids-audit>
<span>
<a href="#">
<i data-feather="x"></i> CLOSE
@@ -139,6 +141,13 @@
<i data-feather="x"></i>
</ids-facet-remove>
</span>
<ids-result name="fail" class="result-icon hidden">
<i data-feather="x"></i>
<span>32</span>
</ids-result>
<ids-result name="pass" class="result-icon hidden">
<i data-feather="check"></i>
</ids-result>
<ids-facet type="requirement"></ids-facet>
<ids-facet-instructions class="requirement-instructions" title="Instructions">
Optionally write instructions about how to achieve this requirement.
@@ -163,7 +172,7 @@
</ids-spec-move>
</span>
</div>
<ids-spec>
</ids-spec>
</template>
</ids-specs>
</div>
+15 -5
View File
@@ -270,20 +270,30 @@ ids-spec ids-spec-handle .snippet {
bottom: -5px;
position: relative;
}
.result-icon {
ids-result {
display: inline-block;
position: absolute;
margin-left: -30px;
margin-top: -2px;
margin-top: -3px;
cursor: help;
}
.result-icon.pass {
ids-result span {
position: absolute;
float: right;
right: 30px;
background-color: #333;
color: white;
font-family: 'Nova Mono';
padding: 3px;
border-radius: 5px;
}
ids-result[name="pass"] {
color: var(--green);
}
.result-icon.fail {
ids-result[name="fail"] {
color: var(--red);
}
.result-icon:hover {
ids-result:hover {
color: var(--blue);
}
.hidden {