mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-23 17:36:29 +00:00
Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0
This commit is contained in:
@@ -132,6 +132,15 @@ def get_enum_items(data, prop_name, context):
|
|||||||
return items
|
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:
|
class IfcHeaderExtractor:
|
||||||
def __init__(self, filepath: str):
|
def __init__(self, filepath: str):
|
||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ classes = (
|
|||||||
operator.EnableEditingGroup,
|
operator.EnableEditingGroup,
|
||||||
operator.DisableEditingGroup,
|
operator.DisableEditingGroup,
|
||||||
operator.SelectGroupProducts,
|
operator.SelectGroupProducts,
|
||||||
|
operator.UpdateGroup,
|
||||||
prop.Group,
|
prop.Group,
|
||||||
prop.BIMGroupProperties,
|
prop.BIMGroupProperties,
|
||||||
ui.BIM_PT_groups,
|
ui.BIM_PT_groups,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import ifcopenshell.api
|
|||||||
import blenderbim.bim.helper
|
import blenderbim.bim.helper
|
||||||
from blenderbim.bim.ifc import IfcStore
|
from blenderbim.bim.ifc import IfcStore
|
||||||
from ifcopenshell.api.group.data import Data
|
from ifcopenshell.api.group.data import Data
|
||||||
|
from ifcopenshell.util.selector import Selector
|
||||||
|
|
||||||
|
|
||||||
class LoadGroups(bpy.types.Operator):
|
class LoadGroups(bpy.types.Operator):
|
||||||
@@ -36,6 +37,7 @@ class LoadGroups(bpy.types.Operator):
|
|||||||
new = props.groups.add()
|
new = props.groups.add()
|
||||||
new.ifc_definition_id = ifc_definition_id
|
new.ifc_definition_id = ifc_definition_id
|
||||||
new.name = group["Name"]
|
new.name = group["Name"]
|
||||||
|
new.selection_query = group["Description"].split("*selector*")[1] if group["Description"] else ""
|
||||||
props.is_editing = True
|
props.is_editing = True
|
||||||
bpy.ops.bim.disable_editing_group()
|
bpy.ops.bim.disable_editing_group()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -167,7 +169,7 @@ class AssignGroup(bpy.types.Operator):
|
|||||||
"group.assign_group",
|
"group.assign_group",
|
||||||
self.file,
|
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),
|
"group": self.file.by_id(self.group),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -219,3 +221,31 @@ class SelectGroupProducts(bpy.types.Operator):
|
|||||||
if self.group in product_groups:
|
if self.group in product_groups:
|
||||||
obj.select_set(True)
|
obj.select_set(True)
|
||||||
return {"FINISHED"}
|
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):
|
class Group(PropertyGroup):
|
||||||
name: StringProperty(name="Name")
|
name: StringProperty(name="Name")
|
||||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||||
|
selection_query: StringProperty(name="Selection Query")
|
||||||
|
|
||||||
|
|
||||||
class BIMGroupProperties(PropertyGroup):
|
class BIMGroupProperties(PropertyGroup):
|
||||||
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
|
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BIM_PT_groups(Panel):
|
|||||||
self.props = context.scene.BIMGroupProperties
|
self.props = context.scene.BIMGroupProperties
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
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:
|
if self.props.is_editing:
|
||||||
row.operator("bim.add_group", text="", icon="ADD")
|
row.operator("bim.add_group", text="", icon="ADD")
|
||||||
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
|
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):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
if item:
|
if item:
|
||||||
row = layout.row(align=True)
|
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
|
group_id = item.ifc_definition_id
|
||||||
if context.scene.BIMGroupProperties.active_group_id == group_id:
|
if context.scene.BIMGroupProperties.active_group_id == group_id:
|
||||||
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
|
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.group = group_id
|
||||||
op = row.operator("bim.remove_group", text="", icon="X")
|
op = row.operator("bim.remove_group", text="", icon="X")
|
||||||
op.group = group_id
|
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:
|
else:
|
||||||
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
|
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
|
||||||
op.group = group_id
|
op.group = group_id
|
||||||
@@ -137,12 +141,18 @@ class BIM_UL_groups(UIList):
|
|||||||
op.group = group_id
|
op.group = group_id
|
||||||
op = row.operator("bim.remove_group", text="", icon="X")
|
op = row.operator("bim.remove_group", text="", icon="X")
|
||||||
op.group = group_id
|
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):
|
class BIM_UL_object_groups(UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
if item:
|
if item:
|
||||||
row = layout.row(align=True)
|
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 = row.operator("bim.assign_group", text="", icon="ADD")
|
||||||
op.group = item.ifc_definition_id
|
op.group = item.ifc_definition_id
|
||||||
|
|||||||
@@ -315,12 +315,16 @@ class AppendLibraryElement(bpy.types.Operator):
|
|||||||
bl_idname = "bim.append_library_element"
|
bl_idname = "bim.append_library_element"
|
||||||
bl_label = "Append Library Element"
|
bl_label = "Append Library Element"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
bl_description = "Append element to the current project"
|
||||||
definition: bpy.props.IntProperty()
|
definition: bpy.props.IntProperty()
|
||||||
prop_index: bpy.props.IntProperty()
|
prop_index: bpy.props.IntProperty()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
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):
|
def execute(self, context):
|
||||||
return IfcStore.execute_ifc_operator(self, context)
|
return IfcStore.execute_ifc_operator(self, context)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ classes = (
|
|||||||
operator.SaveSelectorQuery,
|
operator.SaveSelectorQuery,
|
||||||
operator.OpenQueryLibrary,
|
operator.OpenQueryLibrary,
|
||||||
operator.LoadQuery,
|
operator.LoadQuery,
|
||||||
|
operator.AddToIfcGroup,
|
||||||
prop.BIMFilterClasses,
|
prop.BIMFilterClasses,
|
||||||
prop.BIMFilterBuildingStoreys,
|
prop.BIMFilterBuildingStoreys,
|
||||||
prop.BIMSearchProperties,
|
prop.BIMSearchProperties,
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import re
|
|||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
|
from ifcopenshell.api.group.data import Data
|
||||||
from ifcopenshell.util.selector import Selector
|
from ifcopenshell.util.selector import Selector
|
||||||
import blenderbim.tool as tool
|
import blenderbim.tool as tool
|
||||||
from blenderbim.bim.ifc import IfcStore
|
from blenderbim.bim.ifc import IfcStore
|
||||||
|
from blenderbim.bim.helper import close_operator_panel
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
from bpy.types import PropertyGroup, Operator
|
from bpy.types import PropertyGroup, Operator
|
||||||
from bpy.props import (
|
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="Select All").action = "SELECT"
|
||||||
row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT"
|
row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT"
|
||||||
|
|
||||||
|
|
||||||
class UnhideAllElements(Operator):
|
class UnhideAllElements(Operator):
|
||||||
"""Filter model elements based on selection"""
|
"""Filter model elements based on selection"""
|
||||||
|
|
||||||
@@ -479,7 +480,6 @@ class UnhideAllElements(Operator):
|
|||||||
|
|
||||||
class FilterModelElements(Operator):
|
class FilterModelElements(Operator):
|
||||||
"""Filter model elements based on selection"""
|
"""Filter model elements based on selection"""
|
||||||
|
|
||||||
bl_idname = "bim.filter_model_elements"
|
bl_idname = "bim.filter_model_elements"
|
||||||
bl_label = "Filter Model Elements"
|
bl_label = "Filter Model Elements"
|
||||||
option: StringProperty("select|isolate|hide")
|
option: StringProperty("select|isolate|hide")
|
||||||
@@ -513,7 +513,7 @@ class FilterModelElements(Operator):
|
|||||||
selection = self.add_filters(selection, query)
|
selection = self.add_filters(selection, query)
|
||||||
|
|
||||||
elif query.selector == "GlobalId":
|
elif query.selector == "GlobalId":
|
||||||
selection += f"#{query.global_id}"
|
selection += f"#{query.value}"
|
||||||
|
|
||||||
elif query.selector == "IfcElementType":
|
elif query.selector == "IfcElementType":
|
||||||
index = int(query.active_sub_option.split(":")[0])
|
index = int(query.active_sub_option.split(":")[0])
|
||||||
@@ -562,7 +562,6 @@ class FilterModelElements(Operator):
|
|||||||
|
|
||||||
class IfcSelector(Operator):
|
class IfcSelector(Operator):
|
||||||
"""Select elements in model with IFC Selector"""
|
"""Select elements in model with IFC Selector"""
|
||||||
|
|
||||||
bl_idname = "bim.ifc_selector"
|
bl_idname = "bim.ifc_selector"
|
||||||
bl_label = "Select elements with IFC Selector"
|
bl_label = "Select elements with IFC Selector"
|
||||||
|
|
||||||
@@ -603,19 +602,11 @@ class SaveSelectorQuery(Operator):
|
|||||||
|
|
||||||
class OpenQueryLibrary(Operator):
|
class OpenQueryLibrary(Operator):
|
||||||
"""Open Query Library"""
|
"""Open Query Library"""
|
||||||
|
|
||||||
bl_idname = "bim.open_query_library"
|
bl_idname = "bim.open_query_library"
|
||||||
bl_label = "Open Query Library"
|
bl_label = "Open Query Library"
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
return context.window_manager.invoke_popup(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)
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
@@ -641,8 +632,37 @@ class LoadQuery(Operator):
|
|||||||
bl_idname = "bim.load_query"
|
bl_idname = "bim.load_query"
|
||||||
bl_label = "Load Query"
|
bl_label = "Load Query"
|
||||||
index: IntProperty()
|
index: IntProperty()
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
close_operator_panel(event)
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
ifc_selector = context.scene.IfcSelectorProperties
|
ifc_selector = context.scene.IfcSelectorProperties
|
||||||
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
|
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
|
||||||
return {"FINISHED"}
|
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 = layout.row(align=True)
|
||||||
row.alignment = "CENTER"
|
row.alignment = "CENTER"
|
||||||
row.operator("bim.save_selector_query", text="Save Query")
|
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):
|
def draw_query_group_ui(self, ifc_selector, layout):
|
||||||
for index, group in enumerate(ifc_selector.groups):
|
for index, group in enumerate(ifc_selector.groups):
|
||||||
|
|||||||
@@ -662,7 +662,7 @@ class AddStructuralLoadGroup(bpy.types.Operator):
|
|||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
self.file = IfcStore.get_file()
|
self.file = IfcStore.get_file()
|
||||||
load_group = ifcopenshell.api.run("structural.add_structural_load_group", self.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())
|
Data.load(IfcStore.get_file())
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -754,7 +754,7 @@ class AddStructuralActivity(bpy.types.Operator):
|
|||||||
structural_member=element,
|
structural_member=element,
|
||||||
)
|
)
|
||||||
ifcopenshell.api.run(
|
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())
|
Data.load(IfcStore.get_file())
|
||||||
bpy.ops.bim.enable_editing_structural_load_group_activities(load_group=self.load_group)
|
bpy.ops.bim.enable_editing_structural_load_group_activities(load_group=self.load_group)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from . import schema
|
|||||||
from blenderbim.bim.ifc import IfcStore
|
from blenderbim.bim.ifc import IfcStore
|
||||||
from blenderbim.bim.prop import StrProperty
|
from blenderbim.bim.prop import StrProperty
|
||||||
from blenderbim.bim.ui import IFCFileSelector
|
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 mathutils import Vector, Matrix, Euler
|
||||||
from math import radians
|
from math import radians
|
||||||
|
|
||||||
@@ -531,6 +531,7 @@ def update_enum_property_search_prop(self, context):
|
|||||||
for i, prop in enumerate(self.collection_names):
|
for i, prop in enumerate(self.collection_names):
|
||||||
if prop.name == self.dummy_name:
|
if prop.name == self.dummy_name:
|
||||||
setattr(context.data, self.prop_name, self.collection_identifiers[i].name)
|
setattr(context.data, self.prop_name, self.collection_identifiers[i].name)
|
||||||
|
close_operator_panel(self)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
@@ -542,8 +543,11 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
|
|||||||
collection_names: bpy.props.CollectionProperty(type=StrProperty)
|
collection_names: bpy.props.CollectionProperty(type=StrProperty)
|
||||||
collection_identifiers: bpy.props.CollectionProperty(type=StrProperty)
|
collection_identifiers: bpy.props.CollectionProperty(type=StrProperty)
|
||||||
prop_name: bpy.props.StringProperty()
|
prop_name: bpy.props.StringProperty()
|
||||||
|
mouse_x: bpy.props.IntProperty()
|
||||||
|
mouse_y: bpy.props.IntProperty()
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
self.mouse_x, self.mouse_y = event.mouse_x, event.mouse_y
|
||||||
self.clear_collections()
|
self.clear_collections()
|
||||||
self.data = context.data
|
self.data = context.data
|
||||||
items = get_enum_items(self.data, self.prop_name, context)
|
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"}
|
return {"FINISHED"}
|
||||||
self.add_items_regular(items)
|
self.add_items_regular(items)
|
||||||
self.add_items_suggestions()
|
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):
|
def draw(self, context):
|
||||||
# Mandatory to access context.data in update :
|
# Mandatory to access context.data in update :
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
|
|||||||
)
|
)
|
||||||
group = ifc.run("group.add_group")
|
group = ifc.run("group.add_group")
|
||||||
ifc.run("group.edit_group", group=group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
|
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)
|
collector.assign(camera)
|
||||||
pset = ifc.run("pset.add_pset", product=element, name="EPset_Drawing")
|
pset = ifc.run("pset.add_pset", product=element, name="EPset_Drawing")
|
||||||
ifc.run(
|
ifc.run(
|
||||||
@@ -203,7 +203,7 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
|
|||||||
context=context,
|
context=context,
|
||||||
ifc_representation_class=drawing_tool.get_ifc_representation_class(object_type),
|
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)
|
collector.assign(obj)
|
||||||
drawing_tool.enable_editing(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)
|
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
|
||||||
if annotation:
|
if annotation:
|
||||||
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=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))
|
collector.assign(ifc.get_object(annotation))
|
||||||
|
|
||||||
if reference_obj and ifc.is_moved(reference_obj):
|
if reference_obj and ifc.is_moved(reference_obj):
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ Running tests
|
|||||||
The BlenderBIM Add-on has three layers of tests for each of its three technology
|
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
|
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
|
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
|
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.
|
test coverage to be 100% where it is technically possible to test.
|
||||||
@@ -39,8 +40,8 @@ similar.
|
|||||||
Tool tests
|
Tool tests
|
||||||
----------
|
----------
|
||||||
|
|
||||||
The tool layer tests actual concrete functions. These have the following
|
The tool layer tests actual concrete functions. You will need to install the
|
||||||
dependencies:
|
following dependencies:
|
||||||
|
|
||||||
* pytest-blender, accessible to your system's Python
|
* pytest-blender, accessible to your system's Python
|
||||||
* Blender executable, accessible to pytest-blender on 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``
|
You can install the dependencies by running the ``scripts/setup_pytest.py``
|
||||||
script in Blender.
|
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
|
Then, run the tests. This will launch Blender headlessly and check the behaviour
|
||||||
of all concrete functions.
|
of all concrete functions.
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,40 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# 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/>.
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
import subprocess
|
||||||
|
|
||||||
|
print("Here are the detected system paths:")
|
||||||
|
print(sys.path)
|
||||||
|
|
||||||
py_exec = str(sys.executable)
|
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", "ensurepip", "--user"])
|
||||||
subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
|
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"])
|
sys_paths = [p for p in sys.path if "site-packages" in p]
|
||||||
subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pytest-bdd"])
|
if sys_paths:
|
||||||
subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pygments"])
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +99,15 @@ def i_add_a_sun():
|
|||||||
def i_add_a_material():
|
def i_add_a_material():
|
||||||
bpy.context.active_object.active_material = bpy.data.materials.new("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}"'))
|
@given(parsers.parse('the material "{name}" colour is set to "{colour}"'))
|
||||||
@when(parsers.parse('the material "{name}" colour is set to "{colour}"'))
|
@when(parsers.parse('the material "{name}" colour is set to "{colour}"'))
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class TestAddDrawing:
|
|||||||
ifc.run(
|
ifc.run(
|
||||||
"group.edit_group", group="group", attributes={"Name": "name", "ObjectType": "DRAWING"}
|
"group.edit_group", group="group", attributes={"Name": "name", "ObjectType": "DRAWING"}
|
||||||
).should_be_called()
|
).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()
|
collector.assign("obj").should_be_called()
|
||||||
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
|
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
|
||||||
ifc.run(
|
ifc.run(
|
||||||
@@ -293,7 +293,7 @@ class TestAddAnnotation:
|
|||||||
ifc_representation_class="ifc_representation_class",
|
ifc_representation_class="ifc_representation_class",
|
||||||
).should_be_called().will_return("element")
|
).should_be_called().will_return("element")
|
||||||
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
|
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()
|
collector.assign("obj").should_be_called()
|
||||||
drawing.enable_editing("obj").should_be_called()
|
drawing.enable_editing("obj").should_be_called()
|
||||||
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
|
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ class TestAssign(NewFile):
|
|||||||
tool.Ifc.link(element, element_obj)
|
tool.Ifc.link(element, element_obj)
|
||||||
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
|
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
|
||||||
group.ObjectType = "DRAWING"
|
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)
|
subject.assign(element_obj)
|
||||||
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
|
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
|
||||||
assert bpy.data.collections.get("Views").children.get("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)
|
tool.Ifc.link(element, element_obj)
|
||||||
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
|
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
|
||||||
group.ObjectType = "DRAWING"
|
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)
|
subject.assign(element_obj)
|
||||||
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
|
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
|
||||||
assert bpy.data.collections.get("Views").children.get("IfcGroup/Unnamed")
|
assert bpy.data.collections.get("Views").children.get("IfcGroup/Unnamed")
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ class TestGetDrawingGroup(NewFile):
|
|||||||
tool.Ifc.set(ifc)
|
tool.Ifc.set(ifc)
|
||||||
element = ifc.createIfcAnnotation()
|
element = ifc.createIfcAnnotation()
|
||||||
group = ifcopenshell.api.run("group.add_group", ifc)
|
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
|
assert subject.get_drawing_group(element) == group
|
||||||
|
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ class TestGetGroupElements(NewFile):
|
|||||||
tool.Ifc.set(ifc)
|
tool.Ifc.set(ifc)
|
||||||
element = ifc.createIfcAnnotation()
|
element = ifc.createIfcAnnotation()
|
||||||
group = ifcopenshell.api.run("group.add_group", ifc)
|
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,)
|
assert subject.get_group_elements(group) == (element,)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import ifcopenshell.api
|
|||||||
class Usecase:
|
class Usecase:
|
||||||
def __init__(self, file, **settings):
|
def __init__(self, file, **settings):
|
||||||
self.file = file
|
self.file = file
|
||||||
self.settings = {}
|
self.settings = {
|
||||||
|
"Name": "Unnamed",
|
||||||
|
"Description": "",
|
||||||
|
}
|
||||||
for key, value in settings.items():
|
for key, value in settings.items():
|
||||||
self.settings[key] = value
|
self.settings[key] = value
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ class Usecase:
|
|||||||
**{
|
**{
|
||||||
"GlobalId": ifcopenshell.guid.new(),
|
"GlobalId": ifcopenshell.guid.new(),
|
||||||
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
|
"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(),
|
"GlobalId": ifcopenshell.guid.new(),
|
||||||
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
|
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
|
||||||
"RelatedObjects": [self.settings["product"]],
|
"RelatedObjects": self.settings["product"],
|
||||||
"RelatingGroup": self.settings["group"],
|
"RelatingGroup": self.settings["group"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rel = self.settings["group"].IsGroupedBy[0]
|
rel = self.settings["group"].IsGroupedBy[0]
|
||||||
related_objects = set(rel.RelatedObjects) or set()
|
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)
|
rel.RelatedObjects = list(related_objects)
|
||||||
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
|
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
class Usecase:
|
class Usecase:
|
||||||
def __init__(self, file, **settings):
|
def __init__(self, file, **settings):
|
||||||
self.file = file
|
self.file = file
|
||||||
self.settings = {"group": None, "attributes": {}}
|
self.settings = {
|
||||||
|
"group": None, "attributes": {}}
|
||||||
for key, value in settings.items():
|
for key, value in settings.items():
|
||||||
self.settings[key] = value
|
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 []
|
properties = self.settings["pset"].HasProperties or []
|
||||||
elif self.settings["pset"].is_a("IfcQuantitySet"):
|
elif self.settings["pset"].is_a("IfcQuantitySet"):
|
||||||
properties = self.settings["pset"].Quantities or []
|
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:
|
for prop in properties:
|
||||||
self.file.remove(prop)
|
self.file.remove(prop)
|
||||||
self.file.remove(self.settings["pset"])
|
self.file.remove(self.settings["pset"])
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class Selector:
|
|||||||
value = None
|
value = None
|
||||||
for element in elements:
|
for element in elements:
|
||||||
element_value = cls.get_element_value(element, key)
|
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
|
continue
|
||||||
if comparison and cls.filter_element(element, element_value, comparison, value):
|
if comparison and cls.filter_element(element, element_value, comparison, value):
|
||||||
results.append(element)
|
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("IfcRelDefinesByProperties")) == 0
|
||||||
assert len(self.file.by_type("IfcPropertySet")) == 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):
|
def test_only_unassigning_if_pset_is_used_by_other_elements(self):
|
||||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
||||||
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
||||||
|
|||||||
@@ -1111,7 +1111,7 @@ class TestIdsAuthoring(unittest.TestCase):
|
|||||||
group = ifcopenshell.api.run("group.add_group", ifc)
|
group = ifcopenshell.api.run("group.add_group", ifc)
|
||||||
facet = ids.partOf.create(entity="IfcGroup")
|
facet = ids.partOf.create(entity="IfcGroup")
|
||||||
run("", facet=facet, inst=element, expected=False)
|
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)
|
run("", facet=facet, inst=element, expected=True)
|
||||||
|
|
||||||
# An IfcGroup can be passed by subtypes
|
# 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")
|
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
|
||||||
group = ifc.createIfcInventory()
|
group = ifc.createIfcInventory()
|
||||||
facet = ids.partOf.create(entity="IfcGroup")
|
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)
|
run("", facet=facet, inst=element, expected=True)
|
||||||
|
|
||||||
# An IfcSystem only checks that a system is assigned without any other logic
|
# 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*="oba"]') == [element]
|
||||||
assert subject.Selector.parse(self.file, '.IfcElement[Name*="abc"]') == []
|
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):
|
def test_comparing_if_value_is_in_a_list(self):
|
||||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
||||||
element.Name = "Foobar"
|
element.Name = "Foobar"
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ def cast_to_value(from_value, to_value):
|
|||||||
|
|
||||||
class Facet:
|
class Facet:
|
||||||
def __init__(self, *parameters):
|
def __init__(self, *parameters):
|
||||||
|
self.status = None
|
||||||
self.failed_entities = []
|
self.failed_entities = []
|
||||||
self.failed_reasons = []
|
self.failed_reasons = []
|
||||||
for i, name in enumerate(self.parameters):
|
for i, name in enumerate(self.parameters):
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ class Specification:
|
|||||||
self.applicable_entities.append(element)
|
self.applicable_entities.append(element)
|
||||||
for facet in self.requirements:
|
for facet in self.requirements:
|
||||||
result = facet(element)
|
result = facet(element)
|
||||||
if not bool(result):
|
facet.status = bool(result)
|
||||||
|
if not facet.status:
|
||||||
self.failed_entities.add(element)
|
self.failed_entities.add(element)
|
||||||
facet.failed_entities.append(element)
|
facet.failed_entities.append(element)
|
||||||
facet.failed_reasons.append(str(result))
|
facet.failed_reasons.append(str(result))
|
||||||
@@ -227,5 +228,7 @@ class Specification:
|
|||||||
self.status = False
|
self.status = False
|
||||||
elif self.minOccurs != 0 and not self.applicable_entities:
|
elif self.minOccurs != 0 and not self.applicable_entities:
|
||||||
self.status = False
|
self.status = False
|
||||||
|
for facet in self.requirements:
|
||||||
|
facet.status = False
|
||||||
elif len(self.applicable_entities) > (self.maxOccurs or 1):
|
elif len(self.applicable_entities) > (self.maxOccurs or 1):
|
||||||
self.status = False
|
self.status = False
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ class Reporter:
|
|||||||
def report(self, ids):
|
def report(self, ids):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def to_string():
|
def to_string(self):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def write(filepath):
|
def write(self, filepath):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ class Json(Reporter):
|
|||||||
requirements.append(
|
requirements.append(
|
||||||
{
|
{
|
||||||
"description": requirement.to_string("requirement"),
|
"description": requirement.to_string("requirement"),
|
||||||
"success": not requirement.failed_entities,
|
"status": requirement.status,
|
||||||
"failed_entities": [
|
"failed_entities": [
|
||||||
{"reason": requirement.failed_reasons[i], "element": str(e)}
|
{"reason": requirement.failed_reasons[i], "element": str(e)}
|
||||||
for i, e in enumerate(requirement.failed_entities[0:10])
|
for i, e in enumerate(requirement.failed_entities[0:10])
|
||||||
|
|||||||
@@ -1157,7 +1157,7 @@ class TestPartOf:
|
|||||||
group = ifcopenshell.api.run("group.add_group", ifc)
|
group = ifcopenshell.api.run("group.add_group", ifc)
|
||||||
facet = PartOf(entity="IfcGroup")
|
facet = PartOf(entity="IfcGroup")
|
||||||
run("", facet=facet, inst=element, expected=False)
|
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)
|
run("", facet=facet, inst=element, expected=True)
|
||||||
|
|
||||||
# An IfcGroup can be passed by subtypes
|
# An IfcGroup can be passed by subtypes
|
||||||
@@ -1165,7 +1165,7 @@ class TestPartOf:
|
|||||||
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
|
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
|
||||||
group = ifc.createIfcInventory()
|
group = ifc.createIfcInventory()
|
||||||
facet = PartOf(entity="IfcGroup")
|
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)
|
run("", facet=facet, inst=element, expected=True)
|
||||||
|
|
||||||
# An IfcSystem only checks that a system is assigned without any other logic
|
# An IfcSystem only checks that a system is assigned without any other logic
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -10,6 +10,7 @@ class IDSContainer extends HTMLElement {
|
|||||||
this.filename = 'specifications.ids';
|
this.filename = 'specifications.ids';
|
||||||
this.ids = null;
|
this.ids = null;
|
||||||
this.containerId = crypto.randomUUID();
|
this.containerId = crypto.randomUUID();
|
||||||
|
this.isEditing = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +316,13 @@ class IDSFacets extends HTMLElement {
|
|||||||
}
|
}
|
||||||
feather.replace();
|
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 {
|
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) {
|
renderTemplate(templates, parameters) {
|
||||||
for (var i=0; i<templates.length; i++) {
|
for (var i=0; i<templates.length; i++) {
|
||||||
var hasKeys = true;
|
var hasKeys = true;
|
||||||
@@ -747,6 +770,13 @@ class IDSSpecs extends HTMLElement {
|
|||||||
}
|
}
|
||||||
feather.replace();
|
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 {
|
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) {
|
dragover(e) {
|
||||||
// The HTML draggable API is terrible.
|
// The HTML draggable API is terrible.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -809,7 +848,7 @@ class IDSLoader extends HTMLElement {
|
|||||||
inputElement.accept = '.ids,.xml';
|
inputElement.accept = '.ids,.xml';
|
||||||
inputElement.multiple = false;
|
inputElement.multiple = false;
|
||||||
inputElement.addEventListener("change", this.loadFile)
|
inputElement.addEventListener("change", this.loadFile)
|
||||||
inputElement.dispatchEvent(new MouseEvent("click"));
|
inputElement.dispatchEvent(new MouseEvent("click"));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFile(e) {
|
loadFile(e) {
|
||||||
@@ -845,9 +884,8 @@ class IDSSave extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
click() {
|
click() {
|
||||||
var xmlString = new XMLSerializer().serializeToString(this.closest('ids-container').ids);
|
|
||||||
console.log(xmlString);
|
|
||||||
var container = this.closest('ids-container')
|
var container = this.closest('ids-container')
|
||||||
|
var xmlString = new XMLSerializer().serializeToString(container.ids);
|
||||||
this.download(container.filename, xmlString);
|
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-container', IDSContainer);
|
||||||
window.customElements.define('ids-loader', IDSLoader);
|
window.customElements.define('ids-loader', IDSLoader);
|
||||||
window.customElements.define('ids-save', IDSSave);
|
window.customElements.define('ids-save', IDSSave);
|
||||||
|
window.customElements.define('ids-audit', IDSAudit);
|
||||||
window.customElements.define('ids-info', IDSInfo);
|
window.customElements.define('ids-info', IDSInfo);
|
||||||
window.customElements.define('ids-info-element', IDSInfoElement);
|
window.customElements.define('ids-info-element', IDSInfoElement);
|
||||||
window.customElements.define('ids-specs', IDSSpecs);
|
window.customElements.define('ids-specs', IDSSpecs);
|
||||||
|
|||||||
@@ -47,9 +47,11 @@
|
|||||||
<i data-feather="download"></i> SAVE
|
<i data-feather="download"></i> SAVE
|
||||||
</a>
|
</a>
|
||||||
</ids-save>
|
</ids-save>
|
||||||
<a href="#">
|
<ids-audit>
|
||||||
<i data-feather="play"></i> AUDIT MODEL
|
<a href="#">
|
||||||
</a>
|
<i data-feather="play"></i> AUDIT MODEL
|
||||||
|
</a>
|
||||||
|
</ids-audit>
|
||||||
<span>
|
<span>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<i data-feather="x"></i> CLOSE
|
<i data-feather="x"></i> CLOSE
|
||||||
@@ -139,6 +141,13 @@
|
|||||||
<i data-feather="x"></i>
|
<i data-feather="x"></i>
|
||||||
</ids-facet-remove>
|
</ids-facet-remove>
|
||||||
</span>
|
</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 type="requirement"></ids-facet>
|
||||||
<ids-facet-instructions class="requirement-instructions" title="Instructions">
|
<ids-facet-instructions class="requirement-instructions" title="Instructions">
|
||||||
Optionally write instructions about how to achieve this requirement.
|
Optionally write instructions about how to achieve this requirement.
|
||||||
@@ -163,7 +172,7 @@
|
|||||||
</ids-spec-move>
|
</ids-spec-move>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ids-spec>
|
</ids-spec>
|
||||||
</template>
|
</template>
|
||||||
</ids-specs>
|
</ids-specs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -270,20 +270,30 @@ ids-spec ids-spec-handle .snippet {
|
|||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.result-icon {
|
ids-result {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-left: -30px;
|
margin-left: -30px;
|
||||||
margin-top: -2px;
|
margin-top: -3px;
|
||||||
cursor: help;
|
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);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
.result-icon.fail {
|
ids-result[name="fail"] {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
.result-icon:hover {
|
ids-result:hover {
|
||||||
color: var(--blue);
|
color: var(--blue);
|
||||||
}
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
|
|||||||
Reference in New Issue
Block a user