This commit is contained in:
Chirag Singh
2024-08-01 19:29:48 +05:30
104 changed files with 2362 additions and 778 deletions
+7 -1
View File
@@ -47,6 +47,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install mathutils
pip install src/bcf --no-deps pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
@@ -125,4 +126,9 @@ jobs:
python tests.py python tests.py
cd ../src/ifcopenshell-python cd ../src/ifcopenshell-python
mv ifcopenshell ifcopenshell-local # Force testing on installed module mv ifcopenshell ifcopenshell-local # Force testing on installed module
make test-safe make test
cd ../bcf && make test
pip install requests
cd ../bsdd && make test
cd ../ifcpatch && make test
cd ../ifctester && make test
+29 -2
View File
@@ -1,7 +1,34 @@
[tool.black] [tool.black]
line-length = 120 line-length = 120
include = 'src/(bcf|bcfserver|blenderbim|bsdd|foundationserver|ifc2ca|ifc4d|ifc5d|ifcbimtester|ifcblender|ifccityjson|ifcclash|ifccsv|ifcdiff|ifcfm|ifcpatch|ifctester|ifcopenshell-python|ifcsverchok|opencdeserver)/.*.py$' include = '''
extend-exclude = '(src/ifcopenshell-python/ifcopenshell/express/*|src/ifc2ca/templates/*)' src/(
bcf
|bcfserver
|blenderbim
|bsdd
|foundationserver
|ifc2ca
|ifc4d
|ifc5d
|ifcbimtester
|ifcblender
|ifccityjson
|ifcclash
|ifccsv
|ifcdiff
|ifcfm
|ifcpatch
|ifctester
|ifcopenshell-python
|ifcsverchok
|opencdeserver
)/.*.py$
'''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/*
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifc2ca/templates/*
'''
[tool.pyright] [tool.pyright]
reportInvalidTypeForm = false reportInvalidTypeForm = false
+4
View File
@@ -33,6 +33,10 @@ models:
cd src && xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd cd src && xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd
cd src && xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd cd src && xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd
.PHONY: test
test:
pytest -p no:pytest-blender tests
# .PHONY # .PHONY
# api: # api:
# openapi-python-client generate --url https://api.swaggerhub.com/apis/buildingSMART/BCF/3.0 # openapi-python-client generate --url https://api.swaggerhub.com/apis/buildingSMART/BCF/3.0
+6 -5
View File
@@ -195,19 +195,20 @@ endif
cd build/blenderbim/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css cd build/blenderbim/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
# Provides IFCJSON functionality # Provides IFCJSON functionality
cd build && wget https://github.com/IFCJSON-Team/IFC2JSON_python/archive/master.zip # TODO: replace with main repo if https://github.com/IFCJSON-Team/IFC2JSON_python/pull/3 is merged.
cd build && unzip master.zip && rm master.zip cd build && wget -O ifc2json.zip https://github.com/Moult/IFC2JSON_python/archive/refs/heads/feature-ios-v0.8.0.zip
cd build && unzip ifc2json.zip && rm ifc2json.zip
# IFCJSON doesn't have pyproject.toml, so we use python command. # IFCJSON doesn't have pyproject.toml, so we use python command.
cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-master/file_converters && \ cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-*/file_converters && \
$(PYTHON) -c "from setuptools import setup; \ $(PYTHON) -c "from setuptools import setup; \
setup( \ setup( \
name='ifcjson', \ name='ifcjson', \
version='0.0.0', \ version='0.0.1', \
author='Jan Brouwer', \ author='Jan Brouwer', \
author_email='jan@brewsky.nl', \ author_email='jan@brewsky.nl', \
packages=['ifcjson'], \ packages=['ifcjson'], \
)" bdist_wheel )" bdist_wheel
cp -r build/IFC2JSON_python-master/file_converters/dist/*.whl build/wheels/ cp -r build/IFC2JSON_python-*/file_converters/dist/*.whl build/wheels/
# Brickschema requires pkg_resources which is provided by Blender. # Brickschema requires pkg_resources which is provided by Blender.
# Provides Brickschema functionality # Provides Brickschema functionality
+1 -1
View File
@@ -121,6 +121,7 @@ classes = [
prop.StrProperty, prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty, prop.ObjProperty,
prop.MultipleFileSelect,
prop.Attribute, prop.Attribute,
prop.BIMAreaProperties, prop.BIMAreaProperties,
prop.BIMTabProperties, prop.BIMTabProperties,
@@ -133,7 +134,6 @@ classes = [
prop.BIMMeshProperties, prop.BIMMeshProperties,
prop.BIMFacet, prop.BIMFacet,
prop.BIMFilterGroup, prop.BIMFilterGroup,
prop.MultipleFileSelect,
ui.BIM_UL_clipping_plane, ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic, ui.BIM_UL_generic,
ui.BIM_UL_topics, ui.BIM_UL_topics,
@@ -2,7 +2,9 @@ import sys
import os import os
import webbrowser import webbrowser
blenderbim_lib_path = os.environ.get("blenderbim_lib_path") blenderbim_lib_path = os.environ.get("BLENDERBIM_LIB_PATH")
blenderbim_version = os.environ.get("BLENDERBIM_VERSION")
if blenderbim_lib_path: if blenderbim_lib_path:
sys.path.insert(0, blenderbim_lib_path) sys.path.insert(0, blenderbim_lib_path)
@@ -52,9 +54,16 @@ class WebNamespace(socketio.AsyncNamespace):
"web_operator", "web_operator",
data, data,
namespace="/blender", namespace="/blender",
room=data["blenderId"], room=data.get("blenderId", None),
) )
async def on_get_svg(self, sid, data):
print("hello world!")
file_path = data["path"]
with open(file_path, "r") as file:
svg_data = file.read()
await sio.emit("svg_data", svg_data, room=sid, namespace="/web")
async def send_cached_messages(self, sid): async def send_cached_messages(self, sid):
# Send cached messages to the connected web client # Send cached messages to the connected web client
for blenderId, messages in blender_messages.items(): for blenderId, messages in blender_messages.items():
@@ -98,6 +107,12 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["gantt_data"] = data blender_messages[sid]["gantt_data"] = data
await sio.emit("gantt_data", {"blenderId": sid, "data": data}, namespace="/web") await sio.emit("gantt_data", {"blenderId": sid, "data": data}, namespace="/web")
async def on_drawings_data(self, sid, data):
print(f"Drawings directory from Blender client {sid}")
print(data)
blender_messages[sid]["drwings_data"] = data
await sio.emit("drawings_data", {"blenderId": sid, "data": data}, namespace="/web")
# Attach namespaces # Attach namespaces
sio.register_namespace(WebNamespace("/web")) sio.register_namespace(WebNamespace("/web"))
@@ -108,26 +123,45 @@ sio.register_namespace(BlenderNamespace("/blender"))
async def index(request): async def index(request):
with open("templates/index.html", "r") as f: with open("templates/index.html", "r") as f:
template = f.read() template = f.read()
html_content = pystache.render(template, {"port": sio_port}) html_content = pystache.render(template, {"port": sio_port, "version": blenderbim_version})
return web.Response(text=html_content, content_type="text/html") return web.Response(text=html_content, content_type="text/html")
async def gantt(request): async def gantt(request):
with open("templates/gantt.html", "r") as f: with open("templates/gantt.html", "r") as f:
template = f.read() template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": blenderbim_version})
return web.Response(text=html_content, content_type="text/html")
async def drawings(request):
with open("templates/drawings.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port}) html_content = pystache.render(template, {"port": sio_port})
return web.Response(text=html_content, content_type="text/html") return web.Response(text=html_content, content_type="text/html")
async def open_web_browser(app): async def on_startup(app):
webbrowser.open(f"http://127.0.0.1:{sio_port}/") pid_file = "running_pid.json"
if os.path.exists(pid_file):
with open(pid_file, "r") as f:
pids = json.load(f)
else:
pids = {}
pids[str(os.getpid())] = sio_port
with open(pid_file, "w") as f:
json.dump(pids, f, indent=4)
app.router.add_get("/", index) app.router.add_get("/", index)
app.router.add_get("/drawings", drawings)
app.router.add_get("/gantt", gantt) app.router.add_get("/gantt", gantt)
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt") app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
app.router.add_static("/static/", path="./static", name="static") app.router.add_static("/static/", path="./static", name="static")
app.on_startup.append(open_web_browser) app.on_startup.append(on_startup)
def main(): def main():
@@ -0,0 +1,249 @@
:root {
--font-family: Arial, sans-serif;
--base-font-size: 16px;
--margin-tiny: 0.125rem;
--margin-small: 0.625rem;
--margin-medium: 1.25rem;
--margin-large: 2.5rem;
--padding-tiny: 0.125rem;
--padding-small: 0.625rem;
--padding-medium: 1rem;
--font-size-large: 1.2rem;
--logo-height: 2.5rem;
--nav-height: 2.5rem;
}
:root.dark {
color-scheme: dark;
--bg-color: #252525;
--text-color: #e0e0e0;
--nav-bg-color: #121212;
--nav-border-color: #25682a;
--nav-link-color: #fff;
--nav-link-hover-color: #3fb449;
--warning-color: #FFDB8F;
--border-color: #464444;
}
:root.light {
color-scheme: light;
--bg-color: #ffffff;
--text-color: #000000;
--nav-bg-color: #f8f8f8;
--nav-border-color: #cccccc;
--nav-link-color: #000000;
--nav-link-hover-color: #38a63d;
--warning-color: #FF4500;
--border-color: #222;
}
html {
font-size: var(--base-font-size);
color: var(--text-color);
}
body {
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
font-family: var(--font-family);
}
#container {
margin-top: var(--margin-medium);
margin-left: var(--margin-small);
margin-right: var(--margin-small);
margin-bottom: var(--margin-medium);
height: calc(100vh - var(--nav-height));
box-sizing: border-box;
display: flex;
}
h3 {
margin-top: 0;
}
nav {
background-color: var(--nav-bg-color);
height: var(--nav-height);
padding: var(--padding-medium) 0;
display: flex;
align-items: center;
position: relative;
border-bottom: 2px solid var(--nav-border-color);
}
nav .logo {
margin-left: var(--margin-large);
height: var(--logo-height);
}
nav ul {
list-style-type: none;
margin: 0;
margin-left: 0.0625rem;
display: flex;
flex-grow: 1;
justify-content: center;
}
nav ul li {
margin-right: 3.125rem;
}
nav ul li a {
text-decoration: none;
color: var(--nav-link-color);
font-size: var(--font-size-large);
}
nav ul li a:hover,
nav ul li a.active {
color: var(--nav-link-hover-color);
}
.warning {
color: var(--warning-color);
margin-bottom: var(--margin-small);
padding: var(--padding-tiny);
display: none;
}
#toggle-theme {
border: none;
background: none;
cursor: pointer;
font-size: var(--font-size-large);
margin-right: var(--margin-medium);
}
#toggle-theme:focus {
outline: none;
}
#client-list {
position: absolute;
top: calc(0 + var(--nav-height));
overflow-y: auto;
overflow: hidden;
transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
border: 1px solid var(--table-border-color);
background-color: var(--nav-bg-color);
z-index: 1;
opacity: 0;
visibility: hidden;
width: auto;
}
#client-list.show {
opacity: 1;
visibility: visible;
}
#connected-list-div {
display: inline-block;
}
.client {
padding: var(--margin-small);
border-bottom: 1px solid var(--table-border-color);
cursor: pointer;
}
.client-details {
max-height: 0;
overflow: hidden;
padding-left: var(--padding-small);
transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out;
background-color: var(--nav-bg-color);
margin-top: var(--margin-tiny);
opacity: 0;
}
.client-details.show {
max-height: none;
opacity: 1;
}
.client-detail {
border-bottom: 1px solid var(--table-border-color);
padding: var(--padding-small);
}
#show-connected-button {
width: auto;
background-color: var(--bg-color);
border: none;
font-size: var(--base-font-size);
}
#show-connected-button:hover,
.scroll-button:hover {
cursor: pointer;
}
.scroll-button {
font-size: var(--base-font-size);
margin-left: var(--margin-tiny);
margin-top: var(--margin-small);
}
.left {
flex: 1;
background-color: var(--bg-color);
display: flex;
flex-direction: column;
padding: 20px;
}
.right {
flex: 4;
background-color: var(--bg-color);
border: 1px solid var(--border-color);
}
#svg-container {
height: 100%;
}
.panel {
background-color: var(--nav-bg-color);
border: 1px solid var(--border-color);
padding: 10px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.card-header,
.card-body {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
overflow: auto;
}
.card {
background-color: var(--nav-bg-color);
overflow: auto;
}
li.svg-name {
list-style-type: none;
}
#dropdown-menu:focus {
outline: none;
}
/* remove bootstrap css confilcts */
*,
::after,
::before {
box-sizing: unset;
}
.btn,
.btn:hover {
color: var(--nav-link-hover-color);
}
@@ -1,13 +1,17 @@
:root { :root {
--font-family: Arial, sans-serif; --font-family: Arial, sans-serif;
--base-font-size: 16px;
--margin-tiny: 0.125rem;
--margin-small: 0.625rem; --margin-small: 0.625rem;
--margin-medium: 1.25rem; --margin-medium: 1.25rem;
--margin-large: 2.5rem; --margin-large: 2.5rem;
--padding-small: 0.125rem; --padding-tiny: 0.125rem;
--padding-small: 0.625rem;
--padding-medium: 1rem; --padding-medium: 1rem;
--font-size-small: 1rem; --font-size-small: 1rem;
--font-size-large: 1.2rem; --font-size-large: 1.2rem;
--logo-height: 2.5rem; --logo-height: 2.5rem;
--nav-height: 2.5rem;
--folder-collapse-font-size: 0.75rem; --folder-collapse-font-size: 0.75rem;
--folder-collapse-font-family: Courier, "Courier New", monospace; --folder-collapse-font-family: Courier, "Courier New", monospace;
--box-shadow: 0 0 0.7rem #5f5f5f66; --box-shadow: 0 0 0.7rem #5f5f5f66;
@@ -31,6 +35,7 @@
--group-item-bg-color: #4b4b4b; --group-item-bg-color: #4b4b4b;
--input-bg-color: #3b3b3b; --input-bg-color: #3b3b3b;
--input-border-color: #000; --input-border-color: #000;
--details-border-color: #464444;
} }
:root.light { :root.light {
@@ -42,6 +47,7 @@
--nav-link-color: #000000; --nav-link-color: #000000;
--nav-link-hover-color: #38a63d; --nav-link-hover-color: #38a63d;
--warning-color: #FF4500; --warning-color: #FF4500;
--details-border-color: #222;
} }
html { html {
@@ -53,9 +59,13 @@ body {
color: var(--primary-text-color); color: var(--primary-text-color);
margin: 0; margin: 0;
font-family: var(--font-family); font-family: var(--font-family);
display: flex;
flex-direction: column;
min-height: 100vh;
} }
#container { #container {
flex: 1;
margin-top: var(--margin-medium); margin-top: var(--margin-medium);
margin-left: var(--margin-small); margin-left: var(--margin-small);
margin-right: var(--margin-small); margin-right: var(--margin-small);
@@ -107,7 +117,7 @@ nav ul li a.active {
.warning { .warning {
color: var(--warning-color); color: var(--warning-color);
margin-bottom: var(--margin-small); margin-bottom: var(--margin-small);
padding: var(--padding-small); padding: var(--padding-tiny);
display: none; display: none;
} }
@@ -141,6 +151,87 @@ nav ul li a.active {
outline: none; outline: none;
} }
#client-list {
position: absolute;
top: calc(0 + var(--nav-height));
overflow-y: auto;
overflow: hidden;
transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
border: 1px solid var(--table-border-color);
background-color: var(--nav-bg-color);
z-index: 1;
opacity: 0;
visibility: hidden;
width: auto;
}
#client-list.show {
opacity: 1;
visibility: visible;
}
#connected-list-div {
display: inline-block;
}
.client {
padding: var(--margin-small);
border-bottom: 1px solid var(--table-border-color);
cursor: pointer;
}
.client-details {
max-height: 0;
overflow: hidden;
padding-left: var(--padding-small);
transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out;
background-color: var(--nav-bg-color);
margin-top: var(--margin-tiny);
opacity: 0;
}
.client-details.show {
max-height: none;
opacity: 1;
}
.client-detail {
border-bottom: 1px solid var(--table-border-color);
padding: var(--padding-small);
}
#show-connected-button {
width: auto;
background-color: var(--bg-color);
border: none;
font-size: var(--base-font-size);
}
#show-connected-button:hover,
.scroll-button:hover {
cursor: pointer;
}
.scroll-button {
font-size: var(--base-font-size);
margin-left: var(--margin-tiny);
margin-top: var(--margin-small);
}
footer {
background-color: var(--nav-bg-color);
text-align: right;
padding: var(--padding-tiny);
border-top: 1px solid var(--nav-border-color);
}
footer p {
margin: 0;
color: var(--text-color);
margin-right: var(--margin-small);
font-size: 0.8rem;
}
.gantt-info { .gantt-info {
margin: 0.5rem; margin: 0.5rem;
font-size: var(--font-size-small); font-size: var(--font-size-small);
@@ -1,12 +1,16 @@
:root { :root {
--font-family: Arial, sans-serif; --font-family: Arial, sans-serif;
--base-font-size: 16px;
--margin-tiny: 0.125rem;
--margin-small: 0.625rem; --margin-small: 0.625rem;
--margin-medium: 1.25rem; --margin-medium: 1.25rem;
--margin-large: 2.5rem; --margin-large: 2.5rem;
--padding-small: 0.125rem; --padding-tiny: 0.125rem;
--padding-small: 0.625rem;
--padding-medium: 1rem; --padding-medium: 1rem;
--font-size-large: 1.2rem; --font-size-large: 1.2rem;
--logo-height: 2.5rem; --logo-height: 2.5rem;
--nav-height: 2.5rem;
} }
:root.dark { :root.dark {
@@ -18,6 +22,7 @@
--nav-link-color: #fff; --nav-link-color: #fff;
--nav-link-hover-color: #3fb449; --nav-link-hover-color: #3fb449;
--warning-color: #FFDB8F; --warning-color: #FFDB8F;
--table-border-color: #464444;
} }
:root.light { :root.light {
@@ -29,6 +34,7 @@
--nav-link-color: #000000; --nav-link-color: #000000;
--nav-link-hover-color: #38a63d; --nav-link-hover-color: #38a63d;
--warning-color: #FF4500; --warning-color: #FF4500;
--table-border-color: #222;
} }
html { html {
@@ -40,29 +46,26 @@ body {
color: var(--text-color); color: var(--text-color);
margin: 0; margin: 0;
font-family: var(--font-family); font-family: var(--font-family);
display: flex;
flex-direction: column;
min-height: 100vh;
} }
#container { #container {
flex: 1;
margin-top: var(--margin-medium); margin-top: var(--margin-medium);
margin-left: var(--margin-small); margin-left: var(--margin-small);
margin-right: var(--margin-small); margin-right: var(--margin-small);
margin-bottom: var(--margin-medium); margin-bottom: var(--margin-medium);
} }
.table-container {
margin-bottom: var(--margin-large);
}
.csv-table {
margin-top: var(--margin-small);
}
h3 { h3 {
margin-top: 0; margin-top: 0;
} }
nav { nav {
background-color: var(--nav-bg-color); background-color: var(--nav-bg-color);
height: var(--nav-height);
padding: var(--padding-medium) 0; padding: var(--padding-medium) 0;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -102,7 +105,7 @@ nav ul li a.active {
.warning { .warning {
color: var(--warning-color); color: var(--warning-color);
margin-bottom: var(--margin-small); margin-bottom: var(--margin-small);
padding: var(--padding-small); padding: var(--padding-tiny);
display: none; display: none;
} }
@@ -118,6 +121,95 @@ nav ul li a.active {
outline: none; outline: none;
} }
#client-list {
position: absolute;
top: calc(0 + var(--nav-height));
overflow-y: auto;
overflow: hidden;
transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
border: 1px solid var(--table-border-color);
background-color: var(--nav-bg-color);
z-index: 1;
opacity: 0;
visibility: hidden;
width: auto;
}
#client-list.show {
opacity: 1;
visibility: visible;
}
#connected-list-div {
display: inline-block;
}
.client {
padding: var(--margin-small);
border-bottom: 1px solid var(--table-border-color);
cursor: pointer;
}
.client-details {
max-height: 0;
overflow: hidden;
padding-left: var(--padding-small);
transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out;
background-color: var(--nav-bg-color);
margin-top: var(--margin-tiny);
opacity: 0;
}
.client-details.show {
max-height: none;
opacity: 1;
}
.client-detail {
border-bottom: 1px solid var(--table-border-color);
padding: var(--padding-small);
}
#show-connected-button {
width: auto;
background-color: var(--bg-color);
border: none;
font-size: var(--base-font-size);
}
#show-connected-button:hover,
.scroll-button:hover {
cursor: pointer;
}
.scroll-button {
font-size: var(--base-font-size);
margin-left: var(--margin-tiny);
margin-top: var(--margin-small);
}
footer {
background-color: var(--nav-bg-color);
text-align: right;
padding: var(--padding-tiny);
border-top: 1px solid var(--nav-border-color);
}
footer p {
margin: 0;
color: var(--text-color);
margin-right: var(--margin-small);
font-size: 0.8rem;
}
.table-container {
margin-bottom: var(--margin-large);
}
.csv-table {
margin-top: var(--margin-small);
}
/* ------------ Overwriting Tabulator CSS rules ------------ */ /* ------------ Overwriting Tabulator CSS rules ------------ */
:root.light .tabulator-header, :root.light .tabulator-header,
@@ -127,6 +219,6 @@ nav ul li a.active {
} }
:root.light .tabulator { :root.light .tabulator {
border-top: 1px solid #222; border-top: 1px solid var(--table-border-color);
border-bottom: 2px solid #222; border-bottom: 2px solid var(--table-border-color);
} }
@@ -0,0 +1,263 @@
// keeps track of blenders connected
const connectedClients = {};
const allDrawings = {};
let socket;
// Document ready function
$(document).ready(function () {
var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
var theme = localStorage.getItem("theme") || systemTheme;
setTheme(theme);
$("#dropdown-menu").change(function () {
const blenderId = $(this).attr("id");
const ifcFile = $(this).val();
displayDrawingsNames(blenderId, ifcFile);
});
connectSocket();
});
// Function to connect to Socket.IO server
function connectSocket() {
const url = "ws://localhost:" + SOCKET_PORT + "/web";
socket = io(url);
console.log("socket: ", socket);
// Register socket event handlers
socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("connect", handleWebConnect);
socket.on("drawings_data", handleDrawingsData);
socket.on("svg_data", handleSvgData);
// socket.on("default_data", handleDefaultData);
}
// function used to get drawings data from blenderbim
function handleWebConnect() {
const msg = {
sourcePage: "drawings",
operator: {
type: "getDrawings",
},
};
socket.emit("web_operator", msg);
}
// Function to handle 'blender_connect' event
function handleBlenderConnect(blenderId) {
console.log("blender_connect: ", blenderId);
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = {
shown: false,
ifc_file: "",
};
}
}
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender_disconnect: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
// remove(blenderId);
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) - 1;
});
}
function handleDrawingsData(data) {
const blenderId = data["blenderId"];
console.log(data);
const filename = data["data"]["ifc_file"];
const drawings = data["data"]["drawings_data"]["drawings"];
const sheets = data["data"]["drawings_data"]["sheets"];
allDrawings[filename] = { drawings: drawings, sheets: sheets };
console.log(connectedClients);
if (connectedClients.hasOwnProperty(blenderId)) {
if (!connectedClients[blenderId].shown) {
connectedClients[blenderId] = {
shown: true,
ifc_file: filename,
};
addSelectOption(blenderId, filename);
} else {
// update(blenderId, data, filename);
}
} else {
connectedClients[blenderId] = {
shown: true,
ifc_file: filename,
};
addSelectOption(blenderId, filename);
}
}
function handleSvgData(data) {
const svgContainer = document.getElementById("svg-container");
svgContainer.innerHTML = "";
svgContainer.style.backgroundColor = "white";
const dom = SVG(svgContainer).svg(data);
const svgElement = dom.node.children[0];
// to make the svg not grow outside the svg container
svgElement.setAttribute("width", "100%");
svgElement.setAttribute("height", "100%");
svgElement.style.maxWidth = "100%";
svgElement.style.maxHeight = "100%";
svgElement.style.width = "100%";
svgElement.style.height = "100%";
svgElement.style.boxSizing = "border-box"; // Ensure padding and borders are included in the element's total width and height
const panZoomControls = svgPanZoom(svgElement, {
zoomEnabled: true,
controlIconsEnabled: true,
fit: true,
center: true,
});
}
function addSelectOption(blenderId, filename) {
const filenameOption = $("<option></option>")
.attr("id", "blender-" + blenderId)
.val(filename)
.text(filename);
$("#dropdown-menu").append(filenameOption);
}
function displayDrawingsNames(blenderId, ifcFile) {
drawings = allDrawings[ifcFile].drawings;
sheets = allDrawings[ifcFile].sheets;
function createSvgNames(text, index, type) {
var label = $("<li></lio>")
.text(text)
.attr("id", type + "-" + index)
.addClass("svg-name")
.click(function () {
const id = $(this).attr("id").split("-");
const index = parseInt(id[1]);
const type = id[0];
const ifcFile = $("#dropdown-menu").val();
var path = "";
if (type === "drawing") {
path = allDrawings[ifcFile].drawings[index].path;
} else if (type === "sheet") {
path = allDrawings[ifcFile].sheets[index].path;
}
const msg = {
path: path,
};
console.log(msg);
socket.emit("get_svg", msg);
});
if (type === "drawing") $("#drawings-sub-panel").append(label);
else if (type === "sheet") $("#sheets-sub-panel").append(label);
}
$("#drawings-sub-panel").empty();
drawings.forEach(function (drawing, index) {
createSvgNames(drawing.name, index, "drawing");
});
$("#sheets-sub-panel").empty();
sheets.forEach(function (sheet, index) {
createSvgNames(sheet.name, index, "sheet");
});
}
function setTheme(theme) {
if (theme === "light") {
$("html").removeClass("dark").addClass("light");
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else {
$("html").removeClass("light").addClass("dark");
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
}
localStorage.setItem("theme", theme);
}
function toggleTheme() {
if ($("html").hasClass("dark")) {
setTheme("light");
} else {
setTheme("dark");
}
}
function toggleClientList() {
var clientList = $("#client-list");
if (clientList.hasClass("show")) {
clientList.removeClass("show");
return;
}
clientList.empty();
$.each(connectedClients, function (id, client) {
if (!client.shown) return;
const dropdownIcon = $("<i>")
.addClass("fas fa-chevron-down")
.css("margin-left", "0.5rem");
const clientDiv = $("<div>").addClass("client").text(client.ifc_file);
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
.text(`Blender ID: ${id}`);
clientDetailsDiv.append(clientId);
}
if (client.headers && client.headers.length) {
const clientHeaders = $("<div>")
.addClass("client-detail")
.text(`Table Headers: ${client.headers.join(", ")}`);
const scrollButton = $("<button>")
.addClass("scroll-button")
.text("Scroll to Table")
.on("click", function () {
$("html, body").animate(
{
scrollTop: $("#table-" + id).offset().top,
},
600
);
clientList.removeClass("show");
});
clientDetailsDiv.append(clientHeaders);
clientDetailsDiv.append(scrollButton);
}
clientDiv.append(clientDetailsDiv);
clientDiv.on("click", function () {
clientDetailsDiv.toggleClass("show");
});
clientList.append(clientDiv);
});
clientList.addClass("show");
}
@@ -60,6 +60,10 @@ function handleBlenderDisconnect(blenderId) {
delete connectedClients[blenderId]; delete connectedClients[blenderId];
removeGanttElement(blenderId); removeGanttElement(blenderId);
} }
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) - 1;
});
} }
// Function to handle 'gantt_data' event // Function to handle 'gantt_data' event
@@ -77,6 +81,7 @@ function handleGanttData(data) {
if (!connectedClients[blenderId].shown) { if (!connectedClients[blenderId].shown) {
connectedClients[blenderId] = { connectedClients[blenderId] = {
shown: true, shown: true,
ifc_file: filename,
ganttTasks: ganttTasks, ganttTasks: ganttTasks,
workSchedule: ganttWorkSched, workSchedule: ganttWorkSched,
}; };
@@ -89,6 +94,7 @@ function handleGanttData(data) {
} else { } else {
connectedClients[blenderId] = { connectedClients[blenderId] = {
shown: true, shown: true,
ifc_file: filename,
ganttTasks: ganttTasks, ganttTasks: ganttTasks,
workSchedule: ganttWorkSched, workSchedule: ganttWorkSched,
}; };
@@ -105,6 +111,10 @@ function handleDefaultData(data) {
// Function to add a new gantt with data and filename // Function to add a new gantt with data and filename
function addGanttElement(blenderId, tasks, workSched, filename) { function addGanttElement(blenderId, tasks, workSched, filename) {
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
const ganttContainer = $("<div></div>") const ganttContainer = $("<div></div>")
.addClass("gantt-container") .addClass("gantt-container")
.attr("id", "container-" + blenderId); .attr("id", "container-" + blenderId);
@@ -382,3 +392,75 @@ function toggleTheme() {
setTheme("dark"); setTheme("dark");
} }
} }
function toggleClientList() {
var clientList = $("#client-list");
if (clientList.hasClass("show")) {
clientList.removeClass("show");
return;
}
clientList.empty();
$.each(connectedClients, function (id, client) {
if (!client.shown) return;
const dropdownIcon = $("<i>")
.addClass("fas fa-chevron-down")
.css("margin-left", "0.5rem");
const clientDiv = $("<div>").addClass("client").text(client.ifc_file);
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
.text(`Blender ID: ${id}`);
clientDetailsDiv.append(clientId);
}
if (client.workSchedule && client.gantt) {
const clientScheduleName = $("<div>")
.addClass("client-detail")
.text(`Schedule Name: ${client.workSchedule.Name}`);
const clientScheduleDate = $("<div>")
.addClass("client-detail")
.text(
`Schedule Date: ${new Date(
client.workSchedule.CreationDate
).toLocaleDateString()}`
);
const scrollButton = $("<button>")
.addClass("scroll-button")
.text("Scroll to Gantt Chart")
.on("click", function () {
$("html, body").animate(
{
scrollTop: $("#gantt-" + id).offset().top,
},
600
);
clientList.removeClass("show");
});
clientDetailsDiv.append(clientScheduleName);
clientDetailsDiv.append(clientScheduleDate);
clientDetailsDiv.append(scrollButton);
}
clientDiv.append(clientDetailsDiv);
clientDiv.on("click", function () {
clientDetailsDiv.toggleClass("show");
});
clientList.append(clientDiv);
});
clientList.addClass("show");
}
@@ -42,6 +42,10 @@ function handleBlenderDisconnect(blenderId) {
delete connectedClients[blenderId]; delete connectedClients[blenderId];
removeTableElement(blenderId); removeTableElement(blenderId);
} }
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) - 1;
});
} }
// Function to handle 'csv_data' event // Function to handle 'csv_data' event
@@ -81,6 +85,10 @@ function handleDefaultData(data) {
// Function to add a new table with data and filename // Function to add a new table with data and filename
function addTableElement(blenderId, csvData, filename) { function addTableElement(blenderId, csvData, filename) {
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
// store headers of the csv data // store headers of the csv data
const firstLine = csvData.indexOf("\n"); const firstLine = csvData.indexOf("\n");
const csvHeaders = csvData.substring(0, firstLine).split(","); const csvHeaders = csvData.substring(0, firstLine).split(",");
@@ -274,3 +282,66 @@ function toggleTheme() {
setTheme("dark"); setTheme("dark");
} }
} }
function toggleClientList() {
var clientList = $("#client-list");
if (clientList.hasClass("show")) {
clientList.removeClass("show");
return;
}
clientList.empty();
$.each(connectedClients, function (id, client) {
if (!client.shown) return;
const dropdownIcon = $("<i>")
.addClass("fas fa-chevron-down")
.css("margin-left", "0.5rem");
const clientDiv = $("<div>").addClass("client").text(client.ifc_file);
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
.text(`Blender ID: ${id}`);
clientDetailsDiv.append(clientId);
}
if (client.headers && client.headers.length) {
const clientHeaders = $("<div>")
.addClass("client-detail")
.text(`Table Headers: ${client.headers.join(", ")}`);
const scrollButton = $("<button>")
.addClass("scroll-button")
.text("Scroll to Table")
.on("click", function () {
$("html, body").animate(
{
scrollTop: $("#table-" + id).offset().top,
},
600
);
clientList.removeClass("show");
});
clientDetailsDiv.append(clientHeaders);
clientDetailsDiv.append(scrollButton);
}
clientDiv.append(clientDetailsDiv);
clientDiv.on("click", function () {
clientDetailsDiv.toggleClass("show");
});
clientList.append(clientDiv);
});
clientList.addClass("show");
}
@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Client</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="/static/css/drawings.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/3.1.1/svg.min.js"
></script>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"
></script>
<script
type="text/javascript"
src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
></script>
<script
type="text/javascript"
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
></script>
<script>
var SOCKET_PORT = {{port}};
</script>
<script defer src="./static/js/drawings.js"></script>
</head>
<body>
<nav>
<img
src="https://blenderbim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul>
<li><a href="/">IFC Data</a></li>
<li><a href="/gantt">Gantt Chart</a></li>
<li><a href="/drawings" class="active">Drawings</a></li>
</ul>
<button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
</div>
<div class="container-fluid d-flex" id="container">
<div class="col-2 left">
<!-- Dropdown menu -->
<div class="dropdown mb-3">
<label for="dropdown-menu">IFC File:</label>
<select class="p-0 pl-2" id="dropdown-menu">
<!-- IFC files will be dynamically populated here -->
<option value="" selected disabled hidden>IFC File</option>
</select>
</div>
<!-- Panel with sub-panels -->
<div class="panel">
<div id="accordion">
<div class="card pb-2">
<div class="card-header overflow-hidden" id="headingOne">
<button
class="btn p-0 w-100 text-left"
data-toggle="collapse"
data-target="#collapseOne"
>
Drawings
</button>
</div>
<div id="collapseOne" class="collapse show">
<div class="card-body">
<ul class="sub-panel pl-2" id="drawings-sub-panel">
<!-- Drawings will be dynamically populated here -->
</ul>
</div>
</div>
</div>
<div class="card pb-2">
<div class="card-header overflow-hidden" id="headingTwo">
<button
class="btn p-0 w-100 text-left"
data-toggle="collapse"
data-target="#collapseTwo"
>
Sheets
</button>
</div>
<div id="collapseTwo" class="collapse show">
<div class="card-body">
<ul class="sub-panel pl-2" id="sheets-sub-panel">
<!-- Sheets will be dynamically populated here -->
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-10 right pl-3">
<div id="svg-container"></div>
</div>
</div>
</body>
</html>
@@ -34,11 +34,23 @@
<ul> <ul>
<li><a href="/">IFC Data</a></li> <li><a href="/">IFC Data</a></li>
<li><a href="/gantt" class="active">Gantt Chart</a></li> <li><a href="/gantt" class="active">Gantt Chart</a></li>
<li><a href="/drawings">Drawings</a></li>
</ul> </ul>
<button id="toggle-theme" onclick="toggleTheme()"> <button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i> <i class="fas fa-moon"></i>
</button> </button>
</nav> </nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
</div>
<div id="container"></div> <div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
</footer>
</body> </body>
</html> </html>
@@ -41,11 +41,23 @@
<ul> <ul>
<li><a href="/" class="active">IFC Data</a></li> <li><a href="/" class="active">IFC Data</a></li>
<li><a href="/gantt">Gantt Chart</a></li> <li><a href="/gantt">Gantt Chart</a></li>
<li><a href="/drawings">Drawings</a></li>
</ul> </ul>
<button id="toggle-theme" onclick="toggleTheme()"> <button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i> <i class="fas fa-moon"></i>
</button> </button>
</nav> </nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
</div>
<div id="container"></div> <div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
</footer>
</body> </body>
</html> </html>
+5
View File
@@ -170,6 +170,11 @@ def subscribe_to(obj, data_path, callback):
def refresh_ui_data(): def refresh_ui_data():
"""Refresh cached UI data.
Note that calling non-ifc-operators by itself doesn't refresh the UI data
and it need to be refreshed manually if needed.
"""
from blenderbim.bim import modules from blenderbim.bim import modules
for name, value in modules.items(): for name, value in modules.items():
+2
View File
@@ -50,6 +50,8 @@ def draw_attribute(attribute, layout, copy_operator=None):
return return
if value_name == "enum_value": if value_name == "enum_value":
prop_with_search(layout, attribute, "enum_value", text=attribute.name) prop_with_search(layout, attribute, "enum_value", text=attribute.name)
elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif attribute.name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"]: elif attribute.name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"]:
propis = bpy.context.scene.BIMWorkScheduleProperties propis = bpy.context.scene.BIMWorkScheduleProperties
for item in propis.durations_attributes: for item in propis.durations_attributes:
@@ -167,7 +167,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc, tool.Ifc,
tool.Collector, tool.Collector,
tool.Spatial, tool.Spatial,
structure_obj=tool.Ifc.get_object(current_container), container=current_container,
element_obj=aggregate, element_obj=aggregate,
) )
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj) core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
@@ -21,6 +21,7 @@ import json
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.guid import ifcopenshell.guid
import ifcopenshell.util.element
import blenderbim.bim.helper import blenderbim.bim.helper
import blenderbim.bim.handler import blenderbim.bim.handler
import blenderbim.tool as tool import blenderbim.tool as tool
@@ -36,7 +37,7 @@ class EnableEditingAttributes(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj) obj = bpy.data.objects[self.obj]
props = obj.BIMAttributeProperties props = obj.BIMAttributeProperties
props.attributes.clear() props.attributes.clear()
@@ -1472,12 +1472,9 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall() CutDecorator.uninstall()
# save current visibility statuses for Views and Types collections # save current visibility statuses
visibility_status: dict[bpy.types.Object, bool] = {} visibility_status: dict[bpy.types.Object, bool] = {}
for col in bpy.data.collections["Views"].children: for obj in bpy.data.objects:
for obj in col.objects:
visibility_status[obj] = obj.hide_get()
for obj in bpy.data.collections["Types"].objects:
visibility_status[obj] = obj.hide_get() visibility_status[obj] = obj.hide_get()
if not bpy.app.background: if not bpy.app.background:
@@ -1508,6 +1505,7 @@ class ActivateModel(bpy.types.Operator):
obj.hide_set(hide_status) obj.hide_set(hide_status)
tool.Blender.update_viewport() tool.Blender.update_viewport()
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"} return {"FINISHED"}
@@ -116,7 +116,7 @@ class GeoreferenceDecorator:
arc_mid = angle_half @ arc_start arc_mid = angle_half @ arc_start
self.draw_text_at_position(context, f"{self.tn_angle}deg", arc_mid) self.draw_text_at_position(context, f"{self.tn_angle}deg", arc_mid)
if (wcs := GeoreferenceData.data["world_coordinate_system"]) and ["has_transformation"]: if (wcs := GeoreferenceData.data["world_coordinate_system"]) and wcs["has_transformation"]:
text = "WCS" text = "WCS"
if props.has_blender_offset: if props.has_blender_offset:
text += f"\nBlender Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: {wcs['blender_x']}, Y: {wcs['blender_y']}, Z: {wcs['blender_z']}" text += f"\nBlender Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: {wcs['blender_x']}, Y: {wcs['blender_y']}, Z: {wcs['blender_z']}"
@@ -281,7 +281,7 @@ class GeoreferenceDecorator:
verts, edges = arc_segments verts, edges = arc_segments
self.draw_batch("LINES", verts, decorator_color_special, edges) self.draw_batch("LINES", verts, decorator_color_special, edges)
if (wcs := GeoreferenceData.data["world_coordinate_system"]) and ["has_transformation"]: if (wcs := GeoreferenceData.data["world_coordinate_system"]) and wcs["has_transformation"]:
if wcs["blender_location"].length < 1000: if wcs["blender_location"].length < 1000:
verts = [Vector((0, 0, 0)), wcs["blender_location"]] verts = [Vector((0, 0, 0)), wcs["blender_location"]]
edges = [[0, 1]] edges = [[0, 1]]
@@ -143,7 +143,6 @@ classes = (
stair.EnableEditingStair, stair.EnableEditingStair,
stair.RemoveStair, stair.RemoveStair,
pie.OpenPieClass, pie.OpenPieClass,
pie.PieUpdateContainer,
pie.PieAddOpening, pie.PieAddOpening,
pie.VIEW3D_MT_PIE_bim, pie.VIEW3D_MT_PIE_bim,
pie.VIEW3D_MT_PIE_bim_class, pie.VIEW3D_MT_PIE_bim_class,
@@ -19,6 +19,7 @@
import bpy import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.core.covering as core import blenderbim.core.covering as core
@@ -31,9 +32,7 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id) return tool.Covering.covering_poll_relating_type_check(cls, context, "FLOORING")
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return relating_type == "FLOORING"
def _execute(self, context): def _execute(self, context):
try: try:
@@ -50,9 +49,7 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id) return tool.Covering.covering_poll_relating_type_check(cls, context, "CEILING")
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return relating_type == "CEILING"
def _execute(self, context): def _execute(self, context):
try: try:
@@ -69,8 +66,10 @@ class RegenSelectedCoveringObject(bpy.types.Operator, tool.Ifc.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
element = tool.Ifc.get_entity(bpy.context.active_object) if (obj := context.active_object) and (element := tool.Ifc.get_entity(obj)) and element.is_a("IfcCovering"):
return element and element.is_a("IfcCovering") return True
cls.poll_message_set("IfcCovering must be selected.")
return False
def _execute(self, context): def _execute(self, context):
try: try:
@@ -89,11 +88,7 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
element = tool.Ifc.get_entity(bpy.context.active_object) return tool.Covering.covering_poll_wall_selected(cls, context, "FLOORING")
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element and element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects and relating_type == "FLOORING"
def _execute(self, context): def _execute(self, context):
# This only works based on a 2D plan only considering the standard # This only works based on a 2D plan only considering the standard
@@ -117,11 +112,7 @@ class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
element = tool.Ifc.get_entity(bpy.context.active_object) return tool.Covering.covering_poll_wall_selected(cls, context, "CEILING")
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element and element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects and relating_type == "CEILING"
def _execute(self, context): def _execute(self, context):
# This only works based on a 2D plan only considering the standard # This only works based on a 2D plan only considering the standard
@@ -63,28 +63,6 @@ class PieAddOpening(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class PieUpdateContainer(bpy.types.Operator):
bl_idname = "bim.pie_update_container"
bl_label = "Update Spatial Container"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
for obj in context.selected_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
for collection in obj.users_collection:
spatial_obj = collection.BIMCollectionProperties.obj
if spatial_obj and spatial_obj.BIMObjectProperties.ifc_definition_id:
blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=spatial_obj, element_obj=obj
)
break
return {"FINISHED"}
class VIEW3D_MT_PIE_bim(bpy.types.Menu): class VIEW3D_MT_PIE_bim(bpy.types.Menu):
bl_label = "Geometry" bl_label = "Geometry"
@@ -93,7 +71,6 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu):
pie.operator("bim.edit_object_placement") pie.operator("bim.edit_object_placement")
pie.operator("bim.update_representation").ifc_representation_class = "" pie.operator("bim.update_representation").ifc_representation_class = ""
pie.operator("bim.pie_add_opening") pie.operator("bim.pie_add_opening")
pie.operator("bim.pie_update_container")
pie.operator("bim.open_pie_class", text="Assign IFC Class") pie.operator("bim.open_pie_class", text="Assign IFC Class")
pie.operator("bim.aggregate_assign_object", text="Assign Aggregation") pie.operator("bim.aggregate_assign_object", text="Assign Aggregation")
pie.operator("bim.aggregate_unassign_object", text="Unassign Aggregation") pie.operator("bim.aggregate_unassign_object", text="Unassign Aggregation")
@@ -225,9 +225,8 @@ class AddConstrTypeInstance(bpy.types.Operator):
else: else:
parent = ifcopenshell.util.element.get_container(building_element) parent = ifcopenshell.util.element.get_container(building_element)
if parent: if parent:
parent_obj = tool.Ifc.get_object(parent)
blenderbim.core.spatial.assign_container( blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj tool.Ifc, tool.Collector, tool.Spatial, container=parent, element_obj=obj
) )
# set occurrences properties for the types defined with modifiers # set occurrences properties for the types defined with modifiers
@@ -122,9 +122,14 @@ class UpdateIfcPatchArguments(bpy.types.Operator):
new_attr = patch_args.add() new_attr = patch_args.add()
data_type = arg_info.get("type", "str") data_type = arg_info.get("type", "str")
if isinstance(data_type, list): if isinstance(data_type, list):
if "file" in data_type:
data_type = ["file"]
data_type = [dt for dt in data_type if dt != "NoneType"][0] data_type = [dt for dt in data_type if dt != "NoneType"][0]
new_attr.data_type = { new_attr.data_type = {
"Literal": "enum", "Literal": "enum",
"file": "file",
"str": "string", "str": "string",
"float": "float", "float": "float",
"int": "integer", "int": "integer",
@@ -136,6 +141,11 @@ class UpdateIfcPatchArguments(bpy.types.Operator):
new_attr.enum_value = arg_info.get("default", new_attr.get_value_default()) new_attr.enum_value = arg_info.get("default", new_attr.get_value_default())
continue continue
if new_attr.data_type == "file":
new_attr.filepath_value.single_file = arg_info.get("default", new_attr.get_value_default())
new_attr.filter_glob = arg_info.get("filter_glob", "*.ifc;*.ifczip;*.ifcxml")
continue
new_attr.set_value(arg_info.get("default", new_attr.get_value_default())) new_attr.set_value(arg_info.get("default", new_attr.get_value_default()))
return {"FINISHED"} return {"FINISHED"}
@@ -28,16 +28,15 @@ classes = (
operator.BIM_OT_load_clipping_planes, operator.BIM_OT_load_clipping_planes,
operator.BIM_OT_save_clipping_planes, operator.BIM_OT_save_clipping_planes,
operator.ChangeLibraryElement, operator.ChangeLibraryElement,
operator.ClearRecentIFCProjects,
operator.CreateClippingPlane, operator.CreateClippingPlane,
operator.CreateProject, operator.CreateProject,
operator.ClearRecentIFCProjects,
operator.DisableCulling, operator.DisableCulling,
operator.DisableEditingHeader, operator.DisableEditingHeader,
operator.EditHeader, operator.EditHeader,
operator.EnableCulling, operator.EnableCulling,
operator.EnableEditingHeader, operator.EnableEditingHeader,
operator.ExportIFC, operator.ExportIFC,
operator.ExportIFCDeprecated,
operator.FlipClippingPlane, operator.FlipClippingPlane,
operator.LinkIfc, operator.LinkIfc,
operator.LoadLink, operator.LoadLink,
@@ -92,6 +91,7 @@ def register():
bpy.utils.register_tool(workspace.ExploreTool, after={"builtin.transform"}, separator=True, group=False) bpy.utils.register_tool(workspace.ExploreTool, after={"builtin.transform"}, separator=True, group=False)
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties) bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load) bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load)
bpy.types.TOPBAR_MT_file_import.append(ui.file_import_menu)
bpy.types.TOPBAR_MT_file.prepend(ui.file_menu) bpy.types.TOPBAR_MT_file.prepend(ui.file_menu)
bpy.types.TOPBAR_MT_file_context_menu.prepend(ui.file_menu) bpy.types.TOPBAR_MT_file_context_menu.prepend(ui.file_menu)
wm = bpy.context.window_manager wm = bpy.context.window_manager
@@ -730,12 +730,12 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
if not self.is_existing_ifc_file(): if not self.is_existing_ifc_file():
return {"FINISHED"} return {"FINISHED"}
if tool.Blender.is_default_scene(): if self.should_start_fresh_session and tool.Blender.is_default_scene():
for obj in bpy.data.objects: for obj in bpy.data.objects:
bpy.data.objects.remove(obj) bpy.data.objects.remove(obj)
filepath = Path(self.get_filepath()) filepath = Path(self.get_filepath())
context.scene.BIMProperties.ifc_file = str(filepath) context.scene.BIMProperties.ifc_file = filepath.as_posix()
context.scene.BIMProjectProperties.is_loading = True context.scene.BIMProjectProperties.is_loading = True
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement")) context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
tool.Blender.register_toolbar() tool.Blender.register_toolbar()
@@ -743,6 +743,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
if not self.is_advanced: if not self.is_advanced:
bpy.ops.bim.load_project_elements() bpy.ops.bim.load_project_elements()
if not self.should_start_fresh_session:
bpy.ops.bim.convert_to_blender()
except: except:
blenderbim.last_error = traceback.format_exc() blenderbim.last_error = traceback.format_exc()
raise raise
@@ -1235,6 +1237,10 @@ class ExportIFCBase:
if bpy.data.is_saved: if bpy.data.is_saved:
layout.prop(self, "use_relative_path") layout.prop(self, "use_relative_path")
layout.separator()
layout.label(text="Supported formats for export:")
layout.label(text=".ifc, .ifczip, .ifcjson")
def invoke(self, context, event): def invoke(self, context, event):
if not tool.Ifc.get(): if not tool.Ifc.get():
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT") bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
@@ -1333,18 +1339,6 @@ class ExportIFC(ExportIFCBase, bpy.types.Operator):
pass pass
# TODO: remove as deprecated, better wait couple releases since
# this operator is used for saving IFC files in user scripts.
class ExportIFCDeprecated(ExportIFCBase, bpy.types.Operator):
bl_idname = "export_ifc.bim"
def execute(self, context):
msg = f"'{ExportIFCDeprecated.bl_idname}' operator name is deprecated, use '{ExportIFC.bl_idname}'."
self.report({"WARNING"}, msg)
print(msg)
return super().execute(context)
class LoadLinkedProject(bpy.types.Operator): class LoadLinkedProject(bpy.types.Operator):
bl_idname = "bim.load_linked_project" bl_idname = "bim.load_linked_project"
bl_label = "Load a project for viewing only." bl_label = "Load a project for viewing only."
@@ -25,6 +25,11 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.project.data import ProjectData, LinksData from blenderbim.bim.module.project.data import ProjectData, LinksData
def file_import_menu(self, context):
op = self.layout.operator("bim.load_project", text="IFC (Geometry Only) (.ifc/.ifczip/.ifcxml)")
op.should_start_fresh_session = False
class BIM_MT_project(Menu): class BIM_MT_project(Menu):
bl_idname = "BIM_MT_project" bl_idname = "BIM_MT_project"
bl_label = "New IFC Project" bl_label = "New IFC Project"
@@ -294,9 +299,8 @@ class BIM_PT_new_project_wizard(Panel):
row.prop(props, "volume_unit", text="Volume Unit") row.prop(props, "volume_unit", text="Volume Unit")
prop_with_search(self.layout, pprops, "template_file", text="Template") prop_with_search(self.layout, pprops, "template_file", text="Template")
row = self.layout.row(align=True) row = self.layout.row()
row.operator("bim.create_project") row.operator("bim.create_project")
row.operator("bim.load_project").should_start_fresh_session = False
class BIM_PT_project_library(Panel): class BIM_PT_project_library(Panel):
@@ -25,6 +25,7 @@ classes = (
operator.CalculateFaceAreas, operator.CalculateFaceAreas,
operator.CalculateFormworkArea, operator.CalculateFormworkArea,
operator.CalculateObjectVolumes, operator.CalculateObjectVolumes,
operator.CalculateSideFormworkArea,
operator.CalculateSingleQuantity, operator.CalculateSingleQuantity,
operator.PerformQuantityTakeOff, operator.PerformQuantityTakeOff,
prop.BIMQtoProperties, prop.BIMQtoProperties,
@@ -18,25 +18,28 @@
import bpy import bpy
import bmesh import bmesh
from typing import Callable
def calculate_height(obj): def calculate_height(obj: bpy.types.Object) -> float:
return obj.dimensions[2] return obj.dimensions[2]
def calculate_edges_lengths(objs, context): def calculate_edges_lengths(objs: list[bpy.types.Object], context: bpy.types.Context):
return calculate_mesh_quantity(objs, context, lambda bm: sum((e.calc_length() for e in bm.edges if e.select))) return calculate_mesh_quantity(objs, context, lambda bm: sum((e.calc_length() for e in bm.edges if e.select)))
def calculate_faces_areas(objs, context): def calculate_faces_areas(objs: list[bpy.types.Object], context: bpy.types.Context) -> float:
return calculate_mesh_quantity(objs, context, lambda bm: sum((f.calc_area() for f in bm.faces if f.select))) return calculate_mesh_quantity(objs, context, lambda bm: sum((f.calc_area() for f in bm.faces if f.select)))
def calculate_volumes(objs, context): def calculate_volumes(objs: list[bpy.types.Object], context: bpy.types.Context) -> float:
return calculate_mesh_quantity(objs, context, lambda bm: bm.calc_volume()) return calculate_mesh_quantity(objs, context, lambda bm: bm.calc_volume())
def calculate_mesh_quantity(objs: bpy.types.Object, context, operation): def calculate_mesh_quantity(
objs: list[bpy.types.Object], context: bpy.types.Context, operation: Callable[[bmesh.types.BMesh], float]
) -> float:
"""Get the sum of the target quantity on all passed mesh objects """Get the sum of the target quantity on all passed mesh objects
:param objs: iterable of mesh object :param objs: iterable of mesh object
@@ -58,7 +61,7 @@ def calculate_mesh_quantity(objs: bpy.types.Object, context, operation):
return result return result
def calculate_formwork_area(objs, context): def calculate_formwork_area(objs: list[bpy.types.Object], context: bpy.types.Context) -> float:
""" """
Formwork is defined as the surface area required to cover all exposed Formwork is defined as the surface area required to cover all exposed
surfaces of one or more objects, excluding top surfaces (i.e. that have a surfaces of one or more objects, excluding top surfaces (i.e. that have a
@@ -85,6 +88,7 @@ def calculate_formwork_area(objs, context):
copied_obj.name = "Formwork" copied_obj.name = "Formwork"
copied_obj.BIMObjectProperties.ifc_definition_id = 0 copied_obj.BIMObjectProperties.ifc_definition_id = 0
modifier = copied_obj.modifiers.new("Formwork", "REMESH") modifier = copied_obj.modifiers.new("Formwork", "REMESH")
assert isinstance(modifier, bpy.types.RemeshModifier)
modifier.mode = "SHARP" modifier.mode = "SHARP"
# This hardcoded value may be optimised through a better understanding of the octree division. # This hardcoded value may be optimised through a better understanding of the octree division.
# These values are based off some trial and error heuristics I've learned through experience. # These values are based off some trial and error heuristics I've learned through experience.
@@ -108,7 +112,7 @@ def calculate_formwork_area(objs, context):
return result return result
def calculate_side_formwork_area(objs, context): def calculate_side_formwork_area(objs: list[bpy.types.Object], context: bpy.types.Context) -> float:
""" """
Side formwork is defined as the surface area required to cover all exposed Side formwork is defined as the surface area required to cover all exposed
surfaces of one or more objects, excluding top and bottom surfaces (i.e. surfaces of one or more objects, excluding top and bottom surfaces (i.e.
@@ -99,6 +99,21 @@ class CalculateFormworkArea(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class CalculateSideFormworkArea(bpy.types.Operator):
bl_idname = "bim.calculate_side_formwork_area"
bl_label = "Calculate Side Formwork Area"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects and context.active_object
def execute(self, context):
result = helper.calculate_side_formwork_area([o for o in context.selected_objects if o.type == "MESH"], context)
context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
return {"FINISHED"}
class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator): class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_single_quantity" bl_idname = "bim.calculate_single_quantity"
bl_label = "Calculate Single Quantity" bl_label = "Calculate Single Quantity"
@@ -137,7 +152,10 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.perform_quantity_take_off" bl_idname = "bim.perform_quantity_take_off"
bl_label = "Perform Quantity Take-off" bl_label = "Perform Quantity Take-off"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Perform a quantity take off based of a QTO rule configuration" bl_description = (
"Perform a quantity take off on selected objects based of a QTO rule configuration."
"If no objects are selected, quantities calculated for all available IfcElements."
)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -162,4 +180,6 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules) results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results) ifc5d.qto.edit_qtos(ifc_file, results)
self.report({"INFO"}, f"Quantities are calculated for {len(elements)} elements.")
return {"FINISHED"} return {"FINISHED"}
@@ -175,6 +175,9 @@ class Search(Operator):
props = context.scene.CsvProperties props = context.scene.CsvProperties
elif self.property_group == "BIMSearchProperties": elif self.property_group == "BIMSearchProperties":
props = context.scene.BIMSearchProperties props = context.scene.BIMSearchProperties
else:
raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.")
results = ifcopenshell.util.selector.filter_elements( results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups) tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
) )
@@ -29,11 +29,21 @@ import blenderbim.bim.handler
class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator): class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.reference_structure" bl_idname = "bim.reference_structure"
bl_label = "Reference Structure" bl_label = "Reference Structure"
bl_description = (
"Reference selected objects from all selected structures.\n\n"
"Currently we do not support referencing structures in other structures "
"though it is allowed in IFC4X3"
)
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def _execute(self, context):
objs = tool.Spatial.get_selected_objects_without_containers()
if not objs:
self.report({"INFO"}, "No non-spatial objects are selected.")
return
containers = tool.Spatial.get_selected_containers() containers = tool.Spatial.get_selected_containers()
for obj in context.selected_objects: for obj in objs:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
continue continue
@@ -44,11 +54,21 @@ class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator): class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.dereference_structure" bl_idname = "bim.dereference_structure"
bl_label = "Dereference Structure" bl_label = "Dereference Structure"
bl_description = (
"Dereference selected objects from all selected structures.\n\n"
"Currently we do not support referencing structures in other structures "
"though it is allowed in IFC4X3"
)
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def _execute(self, context):
objs = tool.Spatial.get_selected_objects_without_containers()
if not objs:
self.report({"INFO"}, "No non-spatial objects are selected.")
return
containers = tool.Spatial.get_selected_containers() containers = tool.Spatial.get_selected_containers()
for obj in context.selected_objects: for obj in objs:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
continue continue
@@ -59,6 +79,7 @@ class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
class AssignContainer(bpy.types.Operator, tool.Ifc.Operator): class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_container" bl_idname = "bim.assign_container"
bl_label = "Assign Container" bl_label = "Assign Container"
bl_description = "Assign current default container to the selected objects"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def _execute(self, context):
@@ -99,29 +120,36 @@ class RemoveContainer(bpy.types.Operator, tool.Ifc.Operator):
class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator): class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
""" """
Copies selected 3D elements in the viewport to checkmarked spatial containers Copies selected 3D elements in the viewport to the selected spatial containers
Example: bulk copy a wall to multiple storeys Example: bulk copy a wall to multiple storeys
1. Select one or more 3D elements in the viewport 1. Select one or more 3D elements in the viewport
2. Enable the checkmark next to one or more containers in the container list below to select it 2. Select one or more spatial containers in the viewport
3. Press this button 3. Press this button
4. The copied elements will have a new position relative to the destination containers 4. The copied elements will have a new position relative to the destination containers
"""
Copying containers to other containers currently is not supported."""
bl_idname = "bim.copy_to_container" bl_idname = "bim.copy_to_container"
bl_label = "Copy To Container" bl_label = "Copy To Container"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def _execute(self, context):
# Track decompositions so they can be recreated after the operation objs = tool.Spatial.get_selected_objects_without_containers()
relationships = tool.Root.get_decomposition_relationships(context.selected_objects) if not objs:
old_to_new = {} self.report({"INFO"}, "No non-spatial objects are selected.")
return
containers = tool.Spatial.get_selected_containers() containers = tool.Spatial.get_selected_containers()
for obj in context.selected_objects: # Track decompositions so they can be recreated after the operation
relationships = tool.Root.get_decomposition_relationships(objs)
old_to_new = {}
for obj in objs:
result_objs = core.copy_to_container(tool.Ifc, tool.Collector, tool.Spatial, obj=obj, containers=containers) result_objs = core.copy_to_container(tool.Ifc, tool.Collector, tool.Spatial, obj=obj, containers=containers)
if result_objs: if result_objs:
old_to_new[tool.Ifc.get_entity(obj)] = result_objs old_to_new[tool.Ifc.get_entity(obj)] = result_objs
# Recreate decompositions # Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new) tool.Root.recreate_decompositions(relationships, old_to_new)
blenderbim.bim.handler.refresh_ui_data() blenderbim.bim.handler.refresh_ui_data()
@@ -32,6 +32,7 @@ from bpy.props import (
) )
import blenderbim.tool as tool import blenderbim.tool as tool
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element
def get_subelement_class(self, context): def get_subelement_class(self, context):
@@ -71,6 +72,7 @@ def update_relating_container_from_object(self, context):
return return
container = ifcopenshell.util.element.get_container(element) container = ifcopenshell.util.element.get_container(element)
if container: if container:
# TODO: currently broken and relating_container_object is not used in UI.
bpy.ops.bim.assign_container(structure=container.id()) bpy.ops.bim.assign_container(structure=container.id())
else: else:
bpy.ops.bim.disable_editing_container() bpy.ops.bim.disable_editing_container()
@@ -36,6 +36,10 @@ class BIM_PT_spatial(Panel):
return SpatialData.data["poll"] return SpatialData.data["poll"]
def draw(self, context): def draw(self, context):
# TODO: expose relating_container_object so users could
# assign container without switching default container back and forth
# just for 1 operation.
if not SpatialData.is_loaded: if not SpatialData.is_loaded:
SpatialData.load() SpatialData.load()
@@ -77,9 +81,15 @@ class BIM_PT_spatial(Panel):
else: else:
row.label(text="No Spatial Container") row.label(text="No Spatial Container")
row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="") row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="")
for reference in SpatialData.data["references"]:
references = SpatialData.data["references"]
if references:
self.layout.label(text="Referenced In Structures:")
for reference in references:
row = self.layout.row() row = self.layout.row()
row.label(text=reference, icon="LINKED") row.label(text=reference, icon="LINKED")
else:
self.layout.label(text="No References In Structures")
class BIM_PT_spatial_decomposition(Panel): class BIM_PT_spatial_decomposition(Panel):
@@ -567,5 +567,5 @@ class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def _execute(self, context):
purged_types = core.purge_unused_types(tool.Ifc, tool.Type) purged_types = core.purge_unused_types(tool.Ifc, tool.Type, tool.Geometry)
self.report({"INFO"}, f"{purged_types} types were purged.") self.report({"INFO"}, f"{purged_types} types were purged.")
+1 -1
View File
@@ -147,7 +147,7 @@ class BIM_OT_multiple_file_selector(bpy.types.Operator):
"""Open Blender's file explorer to select one or multiple files.""" """Open Blender's file explorer to select one or multiple files."""
bl_idname = "bim.multiple_file_selector" bl_idname = "bim.multiple_file_selector"
bl_label = "Select Multiple Files" bl_label = "Select File(s)"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement) files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
+36 -28
View File
@@ -156,6 +156,34 @@ class ObjProperty(PropertyGroup):
obj: bpy.props.PointerProperty(type=bpy.types.Object) obj: bpy.props.PointerProperty(type=bpy.types.Object)
def update_single_file(self, context):
self.file_list.clear()
new = self.file_list.add()
new.name = self.single_file
class MultipleFileSelect(PropertyGroup):
single_file: bpy.props.StringProperty(name="Single File Path", description="", update=update_single_file)
file_list: bpy.props.CollectionProperty(type=StrProperty)
def set_file_list(self, dirname: str, files: list[str]):
self.file_list.clear()
for f in files:
new = self.file_list.add()
new.name = os.path.join(dirname, f)
def layout_file_select(self, layout, filter_glob="", text=""):
if len(self.file_list) > 1:
layout.label(text=f"{len(self.file_list)} Files Selected")
else:
layout.prop(self, "single_file", text=text)
layout.context_pointer_set("file_props", self)
op = layout.operator("bim.multiple_file_selector", icon="FILE_FOLDER", text="")
op.filter_glob = filter_glob
def update_attribute_value(self, context): def update_attribute_value(self, context):
value_name = self.get_value_name() value_name = self.get_value_name()
if value_name: if value_name:
@@ -248,6 +276,8 @@ class Attribute(PropertyGroup):
enum_items: StringProperty(name="Value") enum_items: StringProperty(name="Value")
enum_descriptions: CollectionProperty(type=StrProperty) enum_descriptions: CollectionProperty(type=StrProperty)
enum_value: EnumProperty(items=get_attribute_enum_values, name="Value", update=update_attribute_value) enum_value: EnumProperty(items=get_attribute_enum_values, name="Value", update=update_attribute_value)
filepath_value: PointerProperty(type=MultipleFileSelect)
filter_glob: StringProperty()
is_null: BoolProperty(name="Is Null", update=update_is_null) is_null: BoolProperty(name="Is Null", update=update_is_null)
is_optional: BoolProperty(name="Is Optional") is_optional: BoolProperty(name="Is Optional")
is_uri: BoolProperty(name="Is Uri", default=False) is_uri: BoolProperty(name="Is Uri", default=False)
@@ -263,6 +293,8 @@ class Attribute(PropertyGroup):
return None return None
if self.data_type == "string": if self.data_type == "string":
return self.string_value.replace("\\n", "\n") return self.string_value.replace("\\n", "\n")
if self.data_type == "file":
return [f.name for f in self.filepath_value.file_list]
return getattr(self, str(self.get_value_name()), None) return getattr(self, str(self.get_value_name()), None)
def get_value_default(self): def get_value_default(self):
@@ -276,6 +308,8 @@ class Attribute(PropertyGroup):
return False return False
elif self.data_type == "enum": elif self.data_type == "enum":
return "0" return "0"
elif self.data_type == "file":
return ""
def get_value_name(self, display_only=False): def get_value_name(self, display_only=False):
if self.data_type == "string": if self.data_type == "string":
@@ -290,6 +324,8 @@ class Attribute(PropertyGroup):
return "float_value" return "float_value"
elif self.data_type == "enum": elif self.data_type == "enum":
return "enum_value" return "enum_value"
elif self.data_type == "file":
return "filepath_value"
def set_value(self, value): def set_value(self, value):
if isinstance(value, str): if isinstance(value, str):
@@ -478,31 +514,3 @@ class BIMFacet(PropertyGroup):
class BIMFilterGroup(PropertyGroup): class BIMFilterGroup(PropertyGroup):
filters: CollectionProperty(type=BIMFacet, name="filters") filters: CollectionProperty(type=BIMFacet, name="filters")
def update_single_file(self, context):
self.file_list.clear()
new = self.file_list.add()
new.name = self.single_file
class MultipleFileSelect(bpy.types.PropertyGroup):
single_file: bpy.props.StringProperty(name="Single File Path", description="", update=update_single_file)
file_list: bpy.props.CollectionProperty(type=StrProperty)
def set_file_list(self, dirname: str, files: list[str]):
self.file_list.clear()
for f in files:
new = self.file_list.add()
new.name = os.path.join(dirname, f)
def layout_file_select(self, layout, filter_glob="", text=""):
if len(self.file_list) > 1:
layout.label(text=f"{len(self.file_list)} Files Selected")
else:
layout.prop(self, "single_file", text=text)
layout.context_pointer_set("file_props", self)
op = layout.operator("bim.multiple_file_selector", icon="FILE_FOLDER", text="")
op.filter_glob = filter_glob
+18 -8
View File
@@ -16,8 +16,16 @@
# 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
def add_instance_flooring_covering_from_cursor(ifc, root, spatial): if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def add_instance_flooring_covering_from_cursor(ifc: tool.Ifc, root: tool.Root, spatial: tool.Spatial) -> None:
if not root.get_default_container(): if not root.get_default_container():
raise NoDefaultContainer() raise NoDefaultContainer()
@@ -32,7 +40,7 @@ def add_instance_flooring_covering_from_cursor(ifc, root, spatial):
relating_type = None relating_type = None
if selected_objects and active_obj: if selected_objects and active_obj:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
else: else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
@@ -59,7 +67,9 @@ def add_instance_flooring_covering_from_cursor(ifc, root, spatial):
spatial.regen_obj_representation(obj, body) spatial.regen_obj_representation(obj, body)
def add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial): def add_instance_ceiling_covering_from_cursor(
ifc: tool.Ifc, root: tool.Root, covering: tool.Covering, spatial: tool.Spatial
) -> None:
if not root.get_default_container(): if not root.get_default_container():
raise NoDefaultContainer() raise NoDefaultContainer()
@@ -74,7 +84,7 @@ def add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial):
relating_type = None relating_type = None
if selected_objects and active_obj: if selected_objects and active_obj:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
else: else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
ceiling_height = covering.get_z_from_ceiling_height() ceiling_height = covering.get_z_from_ceiling_height()
@@ -101,7 +111,7 @@ def add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial):
spatial.regen_obj_representation(obj, body) spatial.regen_obj_representation(obj, body)
def regen_selected_covering_object(root, spatial): def regen_selected_covering_object(root: tool.Root, spatial: tool.Spatial) -> None:
if not root.get_default_container(): if not root.get_default_container():
raise NoDefaultContainer() raise NoDefaultContainer()
@@ -109,7 +119,7 @@ def regen_selected_covering_object(root, spatial):
selected_objects = spatial.get_selected_objects() selected_objects = spatial.get_selected_objects()
if selected_objects and active_obj: if selected_objects and active_obj:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y) space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
@@ -132,7 +142,7 @@ def regen_selected_covering_object(root, spatial):
# TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS # TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS
def add_instance_flooring_coverings_from_walls(root, spatial): def add_instance_flooring_coverings_from_walls(root: tool.Root, spatial: tool.Spatial) -> None:
if not root.get_default_container(): if not root.get_default_container():
raise NoDefaultContainer() raise NoDefaultContainer()
@@ -158,7 +168,7 @@ def add_instance_flooring_coverings_from_walls(root, spatial):
spatial.regen_obj_representation(obj, body) spatial.regen_obj_representation(obj, body)
def add_instance_ceiling_coverings_from_walls(root, spatial, covering): def add_instance_ceiling_coverings_from_walls(root: tool.Root, spatial: tool.Spatial, covering: tool.Covering) -> None:
if not root.get_default_container(): if not root.get_default_container():
raise NoDefaultContainer() raise NoDefaultContainer()
+7 -2
View File
@@ -18,11 +18,12 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional, Union
if TYPE_CHECKING: if TYPE_CHECKING:
import bpy import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.util.representation
import blenderbim.tool as tool import blenderbim.tool as tool
@@ -225,7 +226,11 @@ def disable_editing_drawings(drawing: tool.Drawing) -> None:
def add_drawing( def add_drawing(
ifc: tool.Ifc, collector: tool.Collector, drawing: tool.Drawing, target_view=None, location_hint=None ifc: tool.Ifc,
collector: tool.Collector,
drawing: tool.Drawing,
target_view: Union[ifcopenshell.util.representation.TARGET_VIEW, None] = None,
location_hint: Union[str, None] = None,
) -> None: ) -> None:
drawing_name = drawing.ensure_unique_drawing_name(drawing.generate_drawing_name(target_view, location_hint)) drawing_name = drawing.ensure_unique_drawing_name(drawing.generate_drawing_name(target_view, location_hint))
drawing_matrix = drawing.generate_drawing_matrix(target_view, location_hint) drawing_matrix = drawing.generate_drawing_matrix(target_view, location_hint)
+6 -4
View File
@@ -126,8 +126,7 @@ def switch_representation(
if not current_obj_data and geometry.is_text_literal(representation): if not current_obj_data and geometry.is_text_literal(representation):
return return
use_immediate_repr = apply_openings and bool(getattr(entity, "HasOpenings", None)) use_immediate_repr = geometry.should_use_immediate_representation(entity, apply_openings)
use_immediate_repr = use_immediate_repr or geometry.has_material_style_override(entity)
if use_immediate_repr: if use_immediate_repr:
# if it has openings make sure to switch to element's mapped representation # if it has openings make sure to switch to element's mapped representation
representation = geometry.unresolve_type_representation(representation, entity) representation = geometry.unresolve_type_representation(representation, entity)
@@ -169,8 +168,11 @@ def get_representation_ifc_parameters(
def remove_representation( def remove_representation(
ifc: tool.Ifc, geometry: tool.Geometry, obj: bpy.types.Object, representation: ifcopenshell.entity_instance ifc: tool.Ifc, geometry: tool.Geometry, obj: bpy.types.Object, representation: ifcopenshell.entity_instance
) -> None: ) -> None:
"""Consider changing obj representation before using the function, """Remove IFC representation from an object.
otherwise it will replace object with empty."""
If removed representation is active will automatically change it to some other one.
If it is the object's only representation, object will be recreated as an empty.
"""
element = ifc.get_entity(obj) element = ifc.get_entity(obj)
assert element assert element
+2 -2
View File
@@ -95,7 +95,7 @@ def copy_to_container(
copied_obj = spatial.duplicate_object_and_data(obj) copied_obj = spatial.duplicate_object_and_data(obj)
spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix) spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix)
result_objs.append(spatial.run_root_copy_class(obj=copied_obj)) result_objs.append(spatial.run_root_copy_class(obj=copied_obj))
spatial.run_spatial_assign_container(structure_obj=to_container_obj, element_obj=copied_obj) spatial.run_spatial_assign_container(container=to_container, element_obj=copied_obj)
spatial.disable_editing(obj) spatial.disable_editing(obj)
return result_objs return result_objs
@@ -173,7 +173,7 @@ def generate_space(ifc: tool.Ifc, model: tool.Model, root: tool.Root, spatial: t
relating_type = None relating_type = None
if selected_objects and active_obj: if selected_objects and active_obj:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
element = ifc.get_entity(active_obj) element = ifc.get_entity(active_obj)
else: else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
+28 -14
View File
@@ -65,10 +65,12 @@ class Demo:
@interface @interface
class Aggregate: class Aggregate:
def can_aggregate(cls, relating_object, related_object): pass def can_aggregate(cls, relating_obj, related_obj): pass
def has_physical_body_representation(cls, element): pass
def disable_editing(cls, obj): pass def disable_editing(cls, obj): pass
def enable_editing(cls, obj): pass def enable_editing(cls, obj): pass
def get_container(cls, element): pass def get_container(cls, element): pass
def get_relating_object(cls, related_element): pass
@interface @interface
@@ -380,6 +382,7 @@ class Geometry:
def clear_modifiers(cls, obj): pass def clear_modifiers(cls, obj): pass
def clear_scale(cls, obj): pass def clear_scale(cls, obj): pass
def delete_data(cls, data): pass def delete_data(cls, data): pass
def delete_ifc_object(cls, obj): pass
def does_representation_id_exist(cls, representation_id): pass def does_representation_id_exist(cls, representation_id): pass
def duplicate_object_data(cls, obj): pass def duplicate_object_data(cls, obj): pass
def get_cartesian_point_coordinate_offset(cls, obj): pass def get_cartesian_point_coordinate_offset(cls, obj): pass
@@ -420,6 +423,9 @@ class Geometry:
def unresolve_type_representation(cls, representation, element): pass def unresolve_type_representation(cls, representation, element): pass
def delete_opening_object_placement(cls, opening): pass def delete_opening_object_placement(cls, opening): pass
def switch_from_representation(cls, obj, representation): pass def switch_from_representation(cls, obj, representation): pass
def get_blender_offset_type(cls, obj): pass
def has_material_style_override(cls, obj): pass
def should_use_immediate_representation(cls, entity, apply_openings): pass
@interface @interface
@@ -447,6 +453,7 @@ class Georeference:
def set_cursor_location(cls, coordinates): pass def set_cursor_location(cls, coordinates): pass
def set_ifc_grid_north(cls): pass def set_ifc_grid_north(cls): pass
def set_ifc_true_north(cls): pass def set_ifc_true_north(cls): pass
def set_model_origin(cls): pass
def set_vector_coordinates(cls, vector_coordinates, type): pass def set_vector_coordinates(cls, vector_coordinates, type): pass
def set_wcs(cls, matrix): pass def set_wcs(cls, matrix): pass
def xyz2enh(cls, coordinates): pass def xyz2enh(cls, coordinates): pass
@@ -845,11 +852,10 @@ class Sequence:
@interface @interface
class Spatial: class Spatial:
def can_contain(cls, structure_obj, element_obj): pass def can_contain(cls, container, element_obj): pass
def can_reference(cls, structure, element): pass def can_reference(cls, structure, element): pass
def contract_container(cls, container): pass def contract_container(cls, container): pass
def copy_xy(cls, src_obj, destination_obj): pass def copy_xy(cls, src_obj, destination_obj): pass
def import_spatial_element(cls, element, level_index): pass
def deselect_objects(cls): pass def deselect_objects(cls): pass
def disable_editing(cls, obj): pass def disable_editing(cls, obj): pass
def duplicate_object_and_data(cls, obj): pass def duplicate_object_and_data(cls, obj): pass
@@ -857,6 +863,9 @@ class Spatial:
def edit_container_name(cls, container, name): pass def edit_container_name(cls, container, name): pass
def enable_editing(cls, obj): pass def enable_editing(cls, obj): pass
def expand_container(cls, container): pass def expand_container(cls, container): pass
def filter_elements_by_class(cls, elements, ifc_class): pass
def filter_elements_by_relating_type(cls, elements, relating_type): pass
def filter_elements_by_untyped(cls, elements): pass
def filter_products(cls, products, action): pass def filter_products(cls, products, action): pass
def get_active_container(cls): pass def get_active_container(cls): pass
def get_container(cls, element): pass def get_container(cls, element): pass
@@ -865,24 +874,27 @@ class Spatial:
def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass
def get_selected_product_types(cls): pass def get_selected_product_types(cls): pass
def get_selected_products(cls): pass def get_selected_products(cls): pass
def import_containers(cls, parent=None): pass
def import_spatial_decomposition(cls): pass def import_spatial_decomposition(cls): pass
def run_root_copy_class(cls, obj=None): pass def import_spatial_element(cls, element, level_index): pass
def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass def load_contained_elements(cls): pass
def run_root_copy_class(cls, obj): pass
def run_spatial_assign_container(cls, container, element_obj): pass
def run_spatial_import_spatial_decomposition(cls): pass def run_spatial_import_spatial_decomposition(cls): pass
def select_object(cls, obj): pass def select_object(cls, obj): pass
def select_products(cls, products, unhide=False): pass def select_products(cls, products, unhide=False): pass
def set_active_object(cls, obj): pass def set_active_object(cls, obj): pass
def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass
def set_default_container(cls, container): pass
def show_scene_objects(cls): pass def show_scene_objects(cls): pass
#HERE STARTS SPATIAL TOOL
# HERE STARTS SPATIAL TOOL
def is_bounding_class(cls, visible_element): pass def is_bounding_class(cls, visible_element): pass
def get_space_polygon_from_context_visible_objects(cls, x, y): pass def get_space_polygon_from_context_visible_objects(cls, x, y): pass
def get_boundary_lines_from_context_visible_objects(cls, visible_objects): pass def debug_shape(cls, foo): pass
def debug_line(cls, start, end): pass
def get_boundary_lines_from_context_visible_objects(cls): pass
def get_gross_mesh_from_element(cls, visible_element): pass def get_gross_mesh_from_element(cls, visible_element): pass
def create_mesh_from_shape(cls, shape): pass def create_mesh_from_shape(cls, shape): pass
def get_x_y_z_h_mat_from_active_obj(cls, active_obj): pass def get_x_y_z_h_mat_from_obj(cls, obj): pass
def get_x_y_z_h_mat_from_cursor(cls): pass def get_x_y_z_h_mat_from_cursor(cls): pass
def get_union_shape_from_selected_objects(cls): pass def get_union_shape_from_selected_objects(cls): pass
def get_boundary_elements(cls, selected_objects): pass def get_boundary_elements(cls, selected_objects): pass
@@ -906,17 +918,20 @@ class Spatial:
def get_active_obj_z(cls): pass def get_active_obj_z(cls): pass
def get_active_obj_height(cls): pass def get_active_obj_height(cls): pass
def get_relating_type_id(cls): pass def get_relating_type_id(cls): pass
def translate_obj_to_z_location(cls, obj): pass def translate_obj_to_z_location(cls, obj, z): pass
def get_2d_vertices_from_obj(cls, obj): pass def get_2d_vertices_from_obj(cls, obj): pass
def get_scaled_2d_vertices(cls, points): pass def get_scaled_2d_vertices(cls, points): pass
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass
def get_body_representation(cls, obj): pass def get_body_representation(cls, obj): pass
def assign_ifcspace_class_to_obj(cls, obj): pass def assign_ifcspace_class_to_obj(cls, obj): pass
def assign_type_to_obj(cls, obj): pass def assign_type_to_obj(cls, obj): pass
def assign_relating_type_to_element(cls, ifc, Type, element, relating_type): pass def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass
def regen_obj_representation(cls, ifc, geometry, obj, body): pass def regen_obj_representation(cls, obj, body): pass
def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass
def toggle_hide_spaces(cls, spaces): pass def toggle_hide_spaces(cls, spaces): pass
def set_default_container(cls, container): pass
def guess_default_container(cls): pass
def get_selected_containers(cls): pass
@interface @interface
class Covering: class Covering:
@@ -1007,7 +1022,6 @@ class Type:
def get_representation_context(cls, representation): pass def get_representation_context(cls, representation): pass
def get_type_occurrences(cls, element_type): pass def get_type_occurrences(cls, element_type): pass
def has_material_usage(cls, element): pass def has_material_usage(cls, element): pass
def remove_object(cls, obj): pass
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
def run_geometry_switch_representation(cls, obj=None, representation=None, should_reload=None, is_global=None): pass def run_geometry_switch_representation(cls, obj=None, representation=None, should_reload=None, is_global=None): pass
+5 -5
View File
@@ -40,15 +40,15 @@ def assign_type(
type_tool.disable_editing(obj) type_tool.disable_editing(obj)
def purge_unused_types(ifc: tool.Ifc, type: tool.Type) -> int: def purge_unused_types(ifc: tool.Ifc, type: tool.Type, geometry: tool.Geometry) -> int:
"""Remove all types without occurrences, return an amount of the removed types.""" """Remove all types without occurrences, return an amount of the removed types."""
purged_types = 0 purged_types = 0
for element_type in type.get_model_types(): for element_type in type.get_model_types():
if not type.get_type_occurrences(element_type): if not type.get_type_occurrences(element_type):
obj = ifc.get_object(element_type) obj = ifc.get_object(element_type)
ifc.run("root.remove_product", product=element_type)
purged_types += 1
if obj: if obj:
ifc.unlink(element=element_type) geometry.delete_ifc_object(obj)
type.remove_object(obj) else:
ifc.run("root.remove_product", product=element_type)
purged_types += 1
return purged_types return purged_types
+4
View File
@@ -6,6 +6,10 @@ def connect_websocket_server(web, port):
# check if port already has a server listening to it # check if port already has a server listening to it
if web.is_port_available(port): if web.is_port_available(port):
web.start_websocket_server(port) web.start_websocket_server(port)
if web.has_started(port):
web.connect_websocket_server(port)
web.open_web_browser(port)
return
web.connect_websocket_server(port) web.connect_websocket_server(port)
+40 -16
View File
@@ -20,6 +20,7 @@ import bpy
import bmesh import bmesh
import shapely import shapely
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.core.root import blenderbim.core.root
import blenderbim.core.spatial import blenderbim.core.spatial
@@ -33,23 +34,46 @@ from shapely import Polygon, MultiPolygon
class Covering(blenderbim.core.tool.Covering): class Covering(blenderbim.core.tool.Covering):
@classmethod @classmethod
def get_z_from_ceiling_height(cls): def get_z_from_ceiling_height(cls) -> float:
props = bpy.context.scene.BIMCoveringProperties props = bpy.context.scene.BIMCoveringProperties
return props.ceiling_height return props.ceiling_height
# def toggle_spaces_visibility_wired_and_textured(cls, spaces):
# first_obj = tool.Ifc.get_object(spaces[0])
# if bpy.data.objects[first_obj.name].display_type == "TEXTURED":
# for space in spaces:
# obj = tool.Ifc.get_object(space)
# bpy.data.objects[obj.name].show_wire = True
# bpy.data.objects[obj.name].display_type = "WIRE"
# return
#
# elif bpy.data.objects[first_obj.name].display_type == "WIRE":
# for space in spaces:
# obj = tool.Ifc.get_object(space)
# bpy.data.objects[obj.name].show_wire = False
# bpy.data.objects[obj.name].display_type = "TEXTURED"
# return
# def toggle_spaces_visibility_wired_and_textured(cls, spaces): @classmethod
# first_obj = tool.Ifc.get_object(spaces[0]) def covering_poll_wall_selected(
# if bpy.data.objects[first_obj.name].display_type == "TEXTURED": cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str
# for space in spaces: ) -> bool:
# obj = tool.Ifc.get_object(space) if not context.selected_objects or not context.active_object:
# bpy.data.objects[obj.name].show_wire = True operator.poll_message_set("No objects selected.")
# bpy.data.objects[obj.name].display_type = "WIRE" return False
# return element = tool.Ifc.get_entity(context.active_object)
# if not element or not element.is_a("IfcWall") or not tool.Model.get_usage_type(element) == "LAYER2":
# elif bpy.data.objects[first_obj.name].display_type == "WIRE": operator.poll_message_set("LAYER2 based IfcWall must be selected.")
# for space in spaces: return False
# obj = tool.Ifc.get_object(space) return cls.covering_poll_relating_type_check(operator, context, covering_type)
# bpy.data.objects[obj.name].show_wire = False
# bpy.data.objects[obj.name].display_type = "TEXTURED" @classmethod
# return def covering_poll_relating_type_check(
cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str
) -> bool:
relating_type_id = int(context.scene.BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if relating_type != covering_type:
operator.poll_message_set(f"Select IfcCoveringType with predefined type '{covering_type}'.")
return False
return True
+127 -92
View File
@@ -19,6 +19,7 @@
import os import os
import re import re
import collections import collections
import collections.abc
import bpy import bpy
import math import math
import json import json
@@ -79,11 +80,11 @@ class Drawing(blenderbim.core.tool.Drawing):
# fmt: on # fmt: on
@classmethod @classmethod
def canonicalise_class_name(cls, name): def canonicalise_class_name(cls, name: str) -> str:
return re.sub("[^0-9a-zA-Z]+", "", name) return re.sub("[^0-9a-zA-Z]+", "", name)
@classmethod @classmethod
def copy_representation(cls, source, dest): def copy_representation(cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance) -> None:
if source.Representation: if source.Representation:
dest.Representation = ifcopenshell.util.element.copy_deep( dest.Representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"] tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"]
@@ -114,7 +115,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return obj return obj
@classmethod @classmethod
def ensure_annotation_in_drawing_plane(cls, obj, camera=None): def ensure_annotation_in_drawing_plane(
cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None
) -> None:
"""Make sure annotation object is going to be visible in the camera view""" """Make sure annotation object is going to be visible in the camera view"""
def get_camera_from_annotation_object(obj): def get_camera_from_annotation_object(obj):
@@ -138,7 +141,9 @@ class Drawing(blenderbim.core.tool.Drawing):
ANNOTATION_TYPES_SUPPORT_SETUP = ("STAIR_ARROW", "TEXT", "REVISION_CLOUD", "FILL_AREA") ANNOTATION_TYPES_SUPPORT_SETUP = ("STAIR_ARROW", "TEXT", "REVISION_CLOUD", "FILL_AREA")
@classmethod @classmethod
def setup_annotation_object(cls, obj, object_type, related_object=None): def setup_annotation_object(
cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None
) -> None:
"""Finish object's adjustments after both object and entity are created""" """Finish object's adjustments after both object and entity are created"""
if not related_object: if not related_object:
@@ -205,7 +210,9 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Drawing.update_text_value(obj) tool.Drawing.update_text_value(obj)
@classmethod @classmethod
def is_annotation_object_type(cls, element, object_types): def is_annotation_object_type(
cls, element: ifcopenshell.entity_instance, object_types: Union[str, list[str]]
) -> bool:
if not isinstance(object_types, collections.abc.Iterable): if not isinstance(object_types, collections.abc.Iterable):
object_types = [object_types] object_types = [object_types]
@@ -226,7 +233,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return False return False
@classmethod @classmethod
def get_annotation_representation(cls, element): def get_annotation_representation(
cls, element: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
rep = ifcopenshell.util.representation.get_representation( rep = ifcopenshell.util.representation.get_representation(
element, "Plan", "Annotation" element, "Plan", "Annotation"
) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation") ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
@@ -237,7 +246,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return rep return rep
@classmethod @classmethod
def create_camera(cls, name, matrix, location_hint): def create_camera(
cls, name: str, matrix: Matrix, location_hint: Literal["PERPSECTIVE", "ORTHOGRAPHIC"]
) -> bpy.types.Object:
camera = bpy.data.objects.new(name, bpy.data.cameras.new(name)) camera = bpy.data.objects.new(name, bpy.data.cameras.new(name))
camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin
camera.data.show_limits = True camera.data.show_limits = True
@@ -257,7 +268,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return camera return camera
@classmethod @classmethod
def create_svg_schedule(cls, schedule): def create_svg_schedule(cls, schedule: ifcopenshell.entity_instance) -> None:
import blenderbim.bim.module.drawing.scheduler as scheduler import blenderbim.bim.module.drawing.scheduler as scheduler
schedule_creator = scheduler.Scheduler() schedule_creator = scheduler.Scheduler()
@@ -276,7 +287,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return uri return uri
@classmethod @classmethod
def add_drawings(cls, sheet): def add_drawings(cls, sheet: ifcopenshell.entity_instance) -> None:
import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.sheeter as sheeter
sheet_builder = sheeter.SheetBuilder() sheet_builder = sheeter.SheetBuilder()
@@ -293,7 +304,7 @@ class Drawing(blenderbim.core.tool.Drawing):
sheet_builder.add_drawing(drawing_references[drawing_annotation.Name], drawing_annotation, sheet) sheet_builder.add_drawing(drawing_references[drawing_annotation.Name], drawing_annotation, sheet)
@classmethod @classmethod
def delete_collection(cls, collection): def delete_collection(cls, collection: bpy.types.Collection) -> None:
bpy.data.collections.remove(collection, do_unlink=True) bpy.data.collections.remove(collection, do_unlink=True)
@classmethod @classmethod
@@ -312,19 +323,19 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.data.objects.remove(obj) bpy.data.objects.remove(obj)
@classmethod @classmethod
def disable_editing_drawings(cls): def disable_editing_drawings(cls) -> None:
bpy.context.scene.DocProperties.is_editing_drawings = False bpy.context.scene.DocProperties.is_editing_drawings = False
@classmethod @classmethod
def disable_editing_schedules(cls): def disable_editing_schedules(cls) -> None:
bpy.context.scene.DocProperties.is_editing_schedules = False bpy.context.scene.DocProperties.is_editing_schedules = False
@classmethod @classmethod
def disable_editing_references(cls): def disable_editing_references(cls) -> None:
bpy.context.scene.DocProperties.is_editing_references = False bpy.context.scene.DocProperties.is_editing_references = False
@classmethod @classmethod
def disable_editing_sheets(cls): def disable_editing_sheets(cls) -> None:
bpy.context.scene.DocProperties.is_editing_sheets = False bpy.context.scene.DocProperties.is_editing_sheets = False
@classmethod @classmethod
@@ -344,19 +355,19 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
@classmethod @classmethod
def enable_editing_drawings(cls): def enable_editing_drawings(cls) -> None:
bpy.context.scene.DocProperties.is_editing_drawings = True bpy.context.scene.DocProperties.is_editing_drawings = True
@classmethod @classmethod
def enable_editing_schedules(cls): def enable_editing_schedules(cls) -> None:
bpy.context.scene.DocProperties.is_editing_schedules = True bpy.context.scene.DocProperties.is_editing_schedules = True
@classmethod @classmethod
def enable_editing_references(cls): def enable_editing_references(cls) -> None:
bpy.context.scene.DocProperties.is_editing_references = True bpy.context.scene.DocProperties.is_editing_references = True
@classmethod @classmethod
def enable_editing_sheets(cls): def enable_editing_sheets(cls) -> None:
bpy.context.scene.DocProperties.is_editing_sheets = True bpy.context.scene.DocProperties.is_editing_sheets = True
@classmethod @classmethod
@@ -368,14 +379,14 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.BIMAssignedProductProperties.is_editing_product = True obj.BIMAssignedProductProperties.is_editing_product = True
@classmethod @classmethod
def ensure_unique_drawing_name(cls, name): def ensure_unique_drawing_name(cls, name: str) -> str:
names = [e.Name for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] names = [e.Name for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]
while name in names: while name in names:
name += "-X" name += "-X"
return name return name
@classmethod @classmethod
def ensure_unique_identification(cls, identification): def ensure_unique_identification(cls, identification: str) -> str:
if tool.Ifc.get_schema() == "IFC2X3": if tool.Ifc.get_schema() == "IFC2X3":
ids = [d.DocumentId for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"] ids = [d.DocumentId for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]
else: else:
@@ -431,7 +442,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", target_view) return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", target_view)
@classmethod @classmethod
def get_body_context(cls): def get_body_context(cls) -> ifcopenshell.entity_instance:
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@classmethod @classmethod
@@ -462,15 +473,15 @@ class Drawing(blenderbim.core.tool.Drawing):
return os.path.splitext(os.path.basename(path))[0] return os.path.splitext(os.path.basename(path))[0]
@classmethod @classmethod
def get_path_with_ext(cls, path, ext): def get_path_with_ext(cls, path: str, ext: str) -> str:
return os.path.splitext(path)[0] + f".{ext}" return os.path.splitext(path)[0] + f".{ext}"
@classmethod @classmethod
def get_unit_system(cls): def get_unit_system(cls) -> Literal["NONE", "METRIC", "IMPERIAL"]:
return bpy.context.scene.unit_settings.system return bpy.context.scene.unit_settings.system
@classmethod @classmethod
def get_drawing_collection(cls, drawing): def get_drawing_collection(cls, drawing: ifcopenshell.entity_instance) -> Union[bpy.types.Collection, None]:
obj = tool.Ifc.get_object(drawing) obj = tool.Ifc.get_object(drawing)
if obj: if obj:
return obj.BIMObjectProperties.collection return obj.BIMObjectProperties.collection
@@ -488,8 +499,8 @@ class Drawing(blenderbim.core.tool.Drawing):
return rel.RelatingDocument return rel.RelatingDocument
@classmethod @classmethod
def get_drawing_references(cls, drawing): def get_drawing_references(cls, drawing: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
results = set() results: set[ifcopenshell.entity_instance] = set()
for inverse in tool.Ifc.get().get_inverse(drawing): for inverse in tool.Ifc.get().get_inverse(drawing):
if inverse.is_a("IfcRelAssignsToProduct") and inverse.RelatingProduct == drawing: if inverse.is_a("IfcRelAssignsToProduct") and inverse.RelatingProduct == drawing:
results.update(inverse.RelatedObjects) results.update(inverse.RelatedObjects)
@@ -505,7 +516,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return rel.RelatedObjects return rel.RelatedObjects
@classmethod @classmethod
def get_ifc_representation_class(cls, object_type): def get_ifc_representation_class(cls, object_type: str) -> str:
if object_type == "TEXT": if object_type == "TEXT":
return "IfcTextLiteral" return "IfcTextLiteral"
elif object_type == "TEXT_LEADER": elif object_type == "TEXT_LEADER":
@@ -517,7 +528,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return element.Name return element.Name
@classmethod @classmethod
def generate_drawing_matrix(cls, target_view, location_hint): def generate_drawing_matrix(
cls, target_view: ifcopenshell.util.representation.TARGET_VIEW, location_hint: str
) -> Matrix:
x, y, z = (0, 0, 0) if location_hint == 0 else bpy.context.scene.cursor.matrix.translation x, y, z = (0, 0, 0) if location_hint == 0 else bpy.context.scene.cursor.matrix.translation
if target_view == "PLAN_VIEW": if target_view == "PLAN_VIEW":
if location_hint: if location_hint:
@@ -554,12 +567,14 @@ class Drawing(blenderbim.core.tool.Drawing):
return mathutils.Matrix() return mathutils.Matrix()
@classmethod @classmethod
def generate_sheet_identification(cls): def generate_sheet_identification(cls) -> str:
number = len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]) number = len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"])
return "A" + str(number).zfill(2) return "A" + str(number).zfill(2)
@classmethod @classmethod
def get_text_literal(cls, obj, return_list=False): def get_text_literal(
cls, obj: bpy.types.Object, return_list: bool = False
) -> Union[ifcopenshell.entity_instance, None, list[ifcopenshell.entity_instance]]:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
return return
@@ -575,11 +590,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return items[0] return items[0]
@classmethod @classmethod
def is_editing_sheets(cls): def is_editing_sheets(cls) -> bool:
return bpy.context.scene.DocProperties.is_editing_sheets return bpy.context.scene.DocProperties.is_editing_sheets
@classmethod @classmethod
def remove_literal_from_annotation(cls, obj, literal): def remove_literal_from_annotation(cls, obj: bpy.types.Object, literal: ifcopenshell.entity_instance) -> None:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
return return
@@ -617,7 +632,9 @@ class Drawing(blenderbim.core.tool.Drawing):
cls.remove_literal_from_annotation(obj, literal) cls.remove_literal_from_annotation(obj, literal)
@classmethod @classmethod
def add_literal_to_annotation(cls, obj, Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left"): def add_literal_to_annotation(
cls, obj: bpy.types.Object, Literal: str = "Literal", Path: str = "RIGHT", BoxAlignment: str = "bottom-left"
) -> Union[ifcopenshell.entity_instance, None]:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
return return
@@ -831,7 +848,7 @@ class Drawing(blenderbim.core.tool.Drawing):
new.identification = schedule.Identification new.identification = schedule.Identification
@classmethod @classmethod
def import_sheets(cls): def import_sheets(cls) -> None:
props = bpy.context.scene.DocProperties props = bpy.context.scene.DocProperties
expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded} expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded}
props.sheets.clear() props.sheets.clear()
@@ -868,7 +885,7 @@ class Drawing(blenderbim.core.tool.Drawing):
new.reference_type = reference_description new.reference_type = reference_description
@classmethod @classmethod
def get_active_sheet(cls, context): def get_active_sheet(cls, context: bpy.types.Context) -> bpy.types.PropertyGroup:
props = context.scene.DocProperties props = context.scene.DocProperties
return next(s for s in props.sheets[: props.active_sheet_index + 1][::-1] if s.is_sheet) return next(s for s in props.sheets[: props.active_sheet_index + 1][::-1] if s.is_sheet)
@@ -904,7 +921,7 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.BIMAssignedProductProperties.relating_product = None obj.BIMAssignedProductProperties.relating_product = None
@classmethod @classmethod
def open_with_user_command(cls, user_command, path): def open_with_user_command(cls, user_command: str, path: str) -> None:
if user_command: if user_command:
commands = json.loads(user_command) commands = json.loads(user_command)
replacements = {"path": path} replacements = {"path": path}
@@ -920,15 +937,15 @@ class Drawing(blenderbim.core.tool.Drawing):
subprocess.call(("xdg-open", path)) subprocess.call(("xdg-open", path))
@classmethod @classmethod
def open_spreadsheet(cls, uri): def open_spreadsheet(cls, uri: str) -> None:
cls.open_with_user_command(tool.Blender.get_addon_preferences().spreadsheet_command, uri) cls.open_with_user_command(tool.Blender.get_addon_preferences().spreadsheet_command, uri)
@classmethod @classmethod
def open_svg(cls, uri): def open_svg(cls, uri: str) -> None:
cls.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, uri) cls.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, uri)
@classmethod @classmethod
def open_layout_svg(cls, uri): def open_layout_svg(cls, uri: str) -> None:
cls.open_with_user_command(tool.Blender.get_addon_preferences().layout_svg_command, uri) cls.open_with_user_command(tool.Blender.get_addon_preferences().layout_svg_command, uri)
@classmethod @classmethod
@@ -954,15 +971,17 @@ class Drawing(blenderbim.core.tool.Drawing):
) )
@classmethod @classmethod
def set_drawing_collection_name(cls, drawing, collection): def set_drawing_collection_name(
cls, drawing: ifcopenshell.entity_instance, collection: bpy.types.Collection
) -> None:
collection.name = tool.Loader.get_name(drawing) collection.name = tool.Loader.get_name(drawing)
@classmethod @classmethod
def set_name(cls, element, name): def set_name(cls, element: ifcopenshell.entity_instance, name: str) -> None:
element.Name = name element.Name = name
@classmethod @classmethod
def show_decorations(cls): def show_decorations(cls) -> None:
bpy.context.scene.DocProperties.should_draw_decorations = True bpy.context.scene.DocProperties.should_draw_decorations = True
@classmethod @classmethod
@@ -1013,15 +1032,15 @@ class Drawing(blenderbim.core.tool.Drawing):
# TODO below this point is highly experimental prototype code with no tests # TODO below this point is highly experimental prototype code with no tests
@classmethod @classmethod
def does_file_exist(cls, uri): def does_file_exist(cls, uri: str) -> bool:
return os.path.exists(uri) return os.path.exists(uri)
@classmethod @classmethod
def delete_file(cls, uri): def delete_file(cls, uri: str) -> None:
os.remove(uri) os.remove(uri)
@classmethod @classmethod
def move_file(cls, src, dest): def move_file(cls, src: str, dest: str) -> None:
try: try:
shutil.move(src, dest) shutil.move(src, dest)
except: except:
@@ -1029,7 +1048,9 @@ class Drawing(blenderbim.core.tool.Drawing):
shutil.copy(src, dest) shutil.copy(src, dest)
@classmethod @classmethod
def generate_drawing_name(cls, target_view, location_hint): def generate_drawing_name(
cls, target_view: ifcopenshell.util.representation.TARGET_VIEW, location_hint: str
) -> str:
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and location_hint: if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and location_hint:
location = tool.Ifc.get().by_id(location_hint) location = tool.Ifc.get().by_id(location_hint)
if target_view == "REFLECTED_PLAN_VIEW": if target_view == "REFLECTED_PLAN_VIEW":
@@ -1042,7 +1063,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return target_view return target_view
@classmethod @classmethod
def get_default_layout_path(cls, identification, name): def get_default_layout_path(cls, identification: str, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0] project = tool.Ifc.get().by_type("IfcProject")[0]
layouts_dir = ( layouts_dir = (
ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir")
@@ -1051,7 +1072,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return os.path.join(layouts_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") return os.path.join(layouts_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/")
@classmethod @classmethod
def get_default_sheet_path(cls, identification, name): def get_default_sheet_path(cls, identification: str, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0] project = tool.Ifc.get().by_type("IfcProject")[0]
sheets_dir = ( sheets_dir = (
ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir")
@@ -1060,7 +1081,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return os.path.join(sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") return os.path.join(sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/")
@classmethod @classmethod
def get_default_titleblock_path(cls, name): def get_default_titleblock_path(cls, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0] project = tool.Ifc.get().by_type("IfcProject")[0]
titleblocks_dir = ( titleblocks_dir = (
ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
@@ -1069,7 +1090,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return os.path.join(titleblocks_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") return os.path.join(titleblocks_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/")
@classmethod @classmethod
def get_default_drawing_path(cls, name): def get_default_drawing_path(cls, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0] project = tool.Ifc.get().by_type("IfcProject")[0]
drawings_dir = ( drawings_dir = (
ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir")
@@ -1078,11 +1099,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return os.path.join(drawings_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") return os.path.join(drawings_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/")
@classmethod @classmethod
def sanitise_filename(cls, name): def sanitise_filename(cls, name: str) -> str:
return "".join(x for x in name if (x.isalnum() or x in "._- ")) return "".join(x for x in name if (x.isalnum() or x in "._- "))
@classmethod @classmethod
def get_default_drawing_resource_path(cls, resource): def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]:
project = tool.Ifc.get().by_type("IfcProject")[0] project = tool.Ifc.get().by_type("IfcProject")[0]
resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr( resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr(
bpy.context.scene.DocProperties, f"{resource.lower()}_path" bpy.context.scene.DocProperties, f"{resource.lower()}_path"
@@ -1091,12 +1112,12 @@ class Drawing(blenderbim.core.tool.Drawing):
return resource_path.replace("\\", "/") return resource_path.replace("\\", "/")
@classmethod @classmethod
def get_default_shading_style(cls): def get_default_shading_style(cls) -> str:
dprops = bpy.context.scene.DocProperties dprops = bpy.context.scene.DocProperties
return dprops.shadingstyle_default return dprops.shadingstyle_default
@classmethod @classmethod
def setup_shading_styles_path(cls, resource_path): def setup_shading_styles_path(cls, resource_path: str) -> None:
resource_path = tool.Ifc.resolve_uri(resource_path) resource_path = tool.Ifc.resolve_uri(resource_path)
os.makedirs(os.path.dirname(resource_path), exist_ok=True) os.makedirs(os.path.dirname(resource_path), exist_ok=True)
if not os.path.exists(resource_path): if not os.path.exists(resource_path):
@@ -1387,7 +1408,7 @@ class Drawing(blenderbim.core.tool.Drawing):
clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW")
elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW") elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW")
def clone(src): def clone(src: bpy.types.Object) -> bpy.types.Object:
dst = src.copy() dst = src.copy()
dst.data = dst.data.copy() dst.data = dst.data.copy()
dst.name = dst.name.replace("IfcGridAxis/", "") dst.name = dst.name.replace("IfcGridAxis/", "")
@@ -1395,13 +1416,13 @@ class Drawing(blenderbim.core.tool.Drawing):
dst.data.BIMMeshProperties.ifc_definition_id = 0 dst.data.BIMMeshProperties.ifc_definition_id = 0
return dst return dst
def disassemble(obj): def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
mesh = bmesh.new() mesh = bmesh.new()
mesh.verts.ensure_lookup_table() mesh.verts.ensure_lookup_table()
mesh.from_mesh(obj.data) mesh.from_mesh(obj.data)
return obj, mesh return obj, mesh
def assemble(obj, mesh): def assemble(obj: bpy.types.Object, mesh: bmesh.types.BMesh) -> bpy.types.Object:
mesh.to_mesh(obj.data) mesh.to_mesh(obj.data)
return obj return obj
@@ -1413,7 +1434,7 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.matrix_world.translation += annotation_offset obj.matrix_world.translation += annotation_offset
return obj, mesh return obj, mesh
def clip_to_camera_boundary(mesh): def clip_to_camera_boundary(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
mesh.verts.ensure_lookup_table() mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]] points = [v.co for v in mesh.verts[0:2]]
points = helper.clip_segment(bounds, points) points = helper.clip_segment(bounds, points)
@@ -1423,7 +1444,7 @@ class Drawing(blenderbim.core.tool.Drawing):
mesh.verts[1].co = points[1] mesh.verts[1].co = points[1]
return mesh return mesh
def draw_grids_vertically(mesh): def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
mesh.verts.ensure_lookup_table() mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]] points = [v.co for v in mesh.verts[0:2]]
points = helper.elevate_segment(bounds, points) points = helper.elevate_segment(bounds, points)
@@ -1534,11 +1555,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return text return text
@classmethod @classmethod
def sync_object_representation(cls, obj): def sync_object_representation(cls, obj: bpy.types.Object) -> None:
bpy.ops.bim.update_representation(obj=obj.name) bpy.ops.bim.update_representation(obj=obj.name)
@classmethod @classmethod
def sync_object_placement(cls, obj): def sync_object_placement(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
blender_matrix = np.array(obj.matrix_world) blender_matrix = np.array(obj.matrix_world)
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if (obj.scale - mathutils.Vector((1.0, 1.0, 1.0))).length > 1e-4: if (obj.scale - mathutils.Vector((1.0, 1.0, 1.0))).length > 1e-4:
@@ -1552,7 +1573,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return element return element
@classmethod @classmethod
def sync_grid_axis_object_placement(cls, obj, element): def sync_grid_axis_object_placement(cls, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0] grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
grid_obj = tool.Ifc.get_object(grid) grid_obj = tool.Ifc.get_object(grid)
if grid_obj: if grid_obj:
@@ -1568,11 +1589,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return document.HasDocumentReferences or [] return document.HasDocumentReferences or []
@classmethod @classmethod
def get_references_with_location(cls, location): def get_references_with_location(cls, location: Union[str, None]) -> list[ifcopenshell.entity_instance]:
return [r for r in tool.Ifc.get().by_type("IfcDocumentReference") if r.Location == location] return [r for r in tool.Ifc.get().by_type("IfcDocumentReference") if r.Location == location]
@classmethod @classmethod
def update_embedded_svg_location(cls, uri, reference, new_location): def update_embedded_svg_location(cls, uri: str, reference: ifcopenshell.entity_instance, new_location: str) -> None:
tree = etree.parse(uri) tree = etree.parse(uri)
root = tree.getroot() root = tree.getroot()
rel_location = os.path.relpath(new_location, os.path.dirname(uri)) rel_location = os.path.relpath(new_location, os.path.dirname(uri))
@@ -1612,11 +1633,13 @@ class Drawing(blenderbim.core.tool.Drawing):
return attributes return attributes
@classmethod @classmethod
def get_reference_location(cls, reference): def get_reference_location(cls, reference: ifcopenshell.entity_instance) -> Union[str, None]:
return reference.Location return reference.Location
@classmethod @classmethod
def get_reference_element(cls, reference): def get_reference_element(
cls, reference: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
if tool.Ifc.get_schema() == "IFC2X3": if tool.Ifc.get_schema() == "IFC2X3":
refs = [r for r in tool.Ifc.get().by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference] refs = [r for r in tool.Ifc.get().by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference]
else: else:
@@ -1625,12 +1648,12 @@ class Drawing(blenderbim.core.tool.Drawing):
return refs[0].RelatedObjects[0] return refs[0].RelatedObjects[0]
@classmethod @classmethod
def get_drawing_human_scale(cls, drawing): def get_drawing_human_scale(cls, drawing: ifcopenshell.entity_instance) -> str:
pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") or {} pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") or {}
return "NTS" if pset.get("IsNTS", False) else pset.get("HumanScale", "NTS") return "NTS" if pset.get("IsNTS", False) else pset.get("HumanScale", "NTS")
@classmethod @classmethod
def get_drawing_metadata(cls, drawing): def get_drawing_metadata(cls, drawing: ifcopenshell.entity_instance) -> list[str]:
# fmt: off # fmt: off
return [ return [
v.strip() v.strip()
@@ -1642,7 +1665,7 @@ class Drawing(blenderbim.core.tool.Drawing):
# fmt: on # fmt: on
@classmethod @classmethod
def get_annotation_z_index(cls, drawing): def get_annotation_z_index(cls, drawing: ifcopenshell.entity_instance) -> float:
return ifcopenshell.util.element.get_pset(drawing, "EPset_Annotation", "ZIndex") or 0 return ifcopenshell.util.element.get_pset(drawing, "EPset_Annotation", "ZIndex") or 0
@classmethod @classmethod
@@ -1654,11 +1677,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return symbol return symbol
@classmethod @classmethod
def has_linework(cls, drawing): def has_linework(cls, drawing: ifcopenshell.entity_instance) -> bool:
return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasLinework", False) return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasLinework", False)
@classmethod @classmethod
def has_annotation(cls, drawing): def has_annotation(cls, drawing: ifcopenshell.entity_instance) -> bool:
return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasAnnotation", False) return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasAnnotation", False)
@classmethod @classmethod
@@ -1723,19 +1746,21 @@ class Drawing(blenderbim.core.tool.Drawing):
return elements return elements
@classmethod @classmethod
def get_annotation_element(cls, element): def get_annotation_element(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in element.HasAssignments: for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"): if rel.is_a("IfcRelAssignsToProduct"):
return rel.RelatingProduct return rel.RelatingProduct
@classmethod @classmethod
def get_drawing_reference(cls, drawing): def get_drawing_reference(cls, drawing: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in drawing.HasAssociations: for rel in drawing.HasAssociations:
if rel.is_a("IfcRelAssociatesDocument"): if rel.is_a("IfcRelAssociatesDocument"):
return rel.RelatingDocument return rel.RelatingDocument
@classmethod @classmethod
def get_reference_document(cls, reference): def get_reference_document(
cls, reference: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
if tool.Ifc.get_schema() == "IFC2X3": if tool.Ifc.get_schema() == "IFC2X3":
return reference.ReferenceToDocument[0] return reference.ReferenceToDocument[0]
return reference.ReferencedDocument return reference.ReferencedDocument
@@ -1749,13 +1774,13 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Ifc.get_object(product).select_set(True) tool.Ifc.get_object(product).select_set(True)
@classmethod @classmethod
def is_drawing_active(cls): def is_drawing_active(cls) -> bool:
camera = bpy.context.scene.camera camera = bpy.context.scene.camera
area = tool.Blender.get_view3d_area() area = tool.Blender.get_view3d_area()
return camera and camera.type == "CAMERA" and camera.BIMObjectProperties.ifc_definition_id and area return camera and camera.type == "CAMERA" and camera.BIMObjectProperties.ifc_definition_id and area
@classmethod @classmethod
def is_camera_orthographic(cls): def is_camera_orthographic(cls) -> bool:
camera = bpy.context.scene.camera camera = bpy.context.scene.camera
return True if (camera and camera.data.type == "ORTHO") else False return True if (camera and camera.data.type == "ORTHO") else False
@@ -1764,7 +1789,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id
@classmethod @classmethod
def run_drawing_activate_model(cls): def run_drawing_activate_model(cls) -> None:
bpy.ops.bim.activate_model() bpy.ops.bim.activate_model()
@classmethod @classmethod
@@ -1906,7 +1931,15 @@ class Drawing(blenderbim.core.tool.Drawing):
) )
@classmethod @classmethod
def is_in_camera_view(cls, obj, camera_inverse_matrix, x, y, clip_start, clip_end): def is_in_camera_view(
cls,
obj: bpy.types.Object,
camera_inverse_matrix: Matrix,
x: float,
y: float,
clip_start: float,
clip_end: float,
) -> bool:
local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box] local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box]
local_x = [v.x for v in local_bbox] local_x = [v.x for v in local_bbox]
local_y = [v.y for v in local_bbox] local_y = [v.y for v in local_bbox]
@@ -1921,14 +1954,14 @@ class Drawing(blenderbim.core.tool.Drawing):
return True return True
@classmethod @classmethod
def is_intersecting_camera(cls, obj, camera): def is_intersecting_camera(cls, obj: bpy.types.Object, camera: bpy.types.Object) -> bool:
# Based on separating axis theorem # Based on separating axis theorem
plane_co = camera.matrix_world.translation plane_co = camera.matrix_world.translation
plane_no = camera.matrix_world.col[2].xyz plane_no = camera.matrix_world.col[2].xyz
return cls.is_intersecting_plane(obj, plane_co, plane_no) return cls.is_intersecting_plane(obj, plane_co, plane_no)
@classmethod @classmethod
def is_intersecting_plane(cls, obj, plane_co, plane_no): def is_intersecting_plane(cls, obj: bpy.types.Object, plane_co: Vector, plane_no: Vector) -> bool:
# Broadphase check using the bounding box # Broadphase check using the bounding box
bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box] bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box]
bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords] bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords]
@@ -1958,7 +1991,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return pos_exists and neg_exists return pos_exists and neg_exists
@classmethod @classmethod
def bisect_mesh(cls, obj, camera): def bisect_mesh(cls, obj: bpy.types.Object, camera: bpy.types.Object) -> tuple[list[Vector], list[list[int]]]:
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
plane_co = camera_matrix.translation plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz plane_no = camera_matrix.col[2].xyz
@@ -1969,7 +2002,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return cls.bisect_mesh_with_plane(obj, plane_co, plane_no, global_offset=global_offset) return cls.bisect_mesh_with_plane(obj, plane_co, plane_no, global_offset=global_offset)
@classmethod @classmethod
def bisect_mesh_with_plane(cls, obj, plane_co, plane_no, global_offset=None): def bisect_mesh_with_plane(
cls, obj: bpy.types.Object, plane_co: Vector, plane_no: Vector, global_offset: Optional[Vector] = None
) -> tuple[list[Vector], list[list[int]]]:
if global_offset is None: if global_offset is None:
global_offset = Vector() global_offset = Vector()
@@ -1980,9 +2015,9 @@ class Drawing(blenderbim.core.tool.Drawing):
geom = bm.verts[:] + bm.edges[:] + bm.faces[:] geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no) results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
vert_map = {} vert_map: dict[int, int] = {}
verts = [] verts: list[Vector] = []
edges = [] edges: list[list[int]] = []
i = 0 i = 0
for geom in results["geom_cut"]: for geom in results["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert): if isinstance(geom, bmesh.types.BMVert):
@@ -1998,12 +2033,12 @@ class Drawing(blenderbim.core.tool.Drawing):
return verts, edges return verts, edges
@classmethod @classmethod
def get_scale_ratio(cls, scale): def get_scale_ratio(cls, scale: str) -> float:
numerator, denominator = scale.split("/") numerator, denominator = scale.split("/")
return float(numerator) / float(denominator) return float(numerator) / float(denominator)
@classmethod @classmethod
def get_diagram_scale(cls, obj): def get_diagram_scale(cls, obj: bpy.types.Object) -> dict[str, float]:
props = obj.data.BIMCameraProperties props = obj.data.BIMCameraProperties
scale = props.diagram_scale scale = props.diagram_scale
if scale != "CUSTOM": if scale != "CUSTOM":
@@ -2029,7 +2064,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return {"HumanScale": human_scale, "Scale": scale} return {"HumanScale": human_scale, "Scale": scale}
@classmethod @classmethod
def convert_scale_string(cls, value): def convert_scale_string(cls, value: str) -> float:
try: try:
return float(value) return float(value)
except: except:
@@ -2060,7 +2095,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return result * 0.0254 return result * 0.0254
@classmethod @classmethod
def extend_line(cls, start, end, distance): def extend_line(cls, start: Vector, end: Vector, distance: float) -> tuple[list[float], list[float]]:
start = np.array(start) start = np.array(start)
end = np.array(end) end = np.array(end)
direction = end - start direction = end - start
@@ -2068,8 +2103,8 @@ class Drawing(blenderbim.core.tool.Drawing):
return (start - offset).tolist(), (end + offset).tolist() return (start - offset).tolist(), (end + offset).tolist()
@classmethod @classmethod
def get_sheet_references(cls, drawing): def get_sheet_references(cls, drawing: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
sheet_references = [] sheet_references: list[ifcopenshell.entity_instance] = []
drawing_reference = cls.get_drawing_document(drawing) drawing_reference = cls.get_drawing_document(drawing)
for sheet in tool.Ifc.get().by_type("IfcDocumentInformation"): for sheet in tool.Ifc.get().by_type("IfcDocumentInformation"):
if not sheet.Scope == "SHEET": if not sheet.Scope == "SHEET":
@@ -2082,7 +2117,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return sheet_references return sheet_references
@classmethod @classmethod
def get_camera_matrix(cls, camera): def get_camera_matrix(cls, camera: bpy.types.Object) -> Matrix:
matrix_world = camera.matrix_world.copy().normalized() matrix_world = camera.matrix_world.copy().normalized()
location, rotation, scale = matrix_world.decompose() location, rotation, scale = matrix_world.decompose()
if scale.x < 0 or scale.y < 0 or scale.z < 0: if scale.x < 0 or scale.y < 0 or scale.z < 0:
@@ -927,6 +927,7 @@ class Geometry(blenderbim.core.tool.Geometry):
new_representation = None new_representation = None
for r in cls.get_representations_iter(element): for r in cls.get_representations_iter(element):
r = tool.Geometry.resolve_mapped_representation(r)
if r != representation: if r != representation:
new_representation = r new_representation = r
break break
@@ -1227,3 +1228,9 @@ class Geometry(blenderbim.core.tool.Geometry):
return return
material_style = tool.Material.get_style(materials[0]) material_style = tool.Material.get_style(materials[0])
return material_style return material_style
@classmethod
def should_use_immediate_representation(cls, element: ifcopenshell.entity_instance, apply_openings: bool) -> bool:
use_immediate_repr = apply_openings and bool(getattr(element, "HasOpenings", None))
use_immediate_repr = use_immediate_repr or cls.has_material_style_override(element)
return use_immediate_repr
@@ -21,6 +21,9 @@ import json
import numpy as np import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.api.georeference import ifcopenshell.api.georeference
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.bim.helper import blenderbim.bim.helper
+62 -37
View File
@@ -1,3 +1,22 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os import os
import re import re
import bpy import bpy
@@ -5,6 +24,7 @@ import logging
from blenderbim.bim import import_ifc from blenderbim.bim import import_ifc
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool import blenderbim.tool as tool
from typing import TYPE_CHECKING, Union
# allows git import even if git executable isn't found # allows git import even if git executable isn't found
os.environ["GIT_PYTHON_REFRESH"] = "quiet" os.environ["GIT_PYTHON_REFRESH"] = "quiet"
@@ -13,15 +33,20 @@ try:
except ImportError: except ImportError:
print("Warning: GitPython not available.") print("Warning: GitPython not available.")
if TYPE_CHECKING:
import git
class IfcGit: class IfcGit:
STEP_IDS = dict[str, set[int]]
@classmethod @classmethod
def init_repo(cls, path_dir): def init_repo(cls, path_dir: str) -> None:
IfcGitRepo.repo = git.Repo.init(path_dir) IfcGitRepo.repo = git.Repo.init(path_dir)
cls.config_info_attributes(IfcGitRepo.repo) cls.config_info_attributes(IfcGitRepo.repo)
@classmethod @classmethod
def clone_repo(cls, remote_url, local_folder): def clone_repo(cls, remote_url: str, local_folder: str) -> git.Repo:
IfcGitRepo.repo = git.Repo.clone_from( IfcGitRepo.repo = git.Repo.clone_from(
url=remote_url, url=remote_url,
to_path=local_folder, to_path=local_folder,
@@ -30,7 +55,7 @@ class IfcGit:
return IfcGitRepo.repo return IfcGitRepo.repo
@classmethod @classmethod
def load_anyifc(cls, repo): def load_anyifc(cls, repo: git.Repo) -> bool:
working_dir = repo.working_dir working_dir = repo.working_dir
for item in os.listdir(working_dir): for item in os.listdir(working_dir):
path = os.path.join(working_dir, item) path = os.path.join(working_dir, item)
@@ -40,11 +65,11 @@ class IfcGit:
return False return False
@classmethod @classmethod
def get_path_dir(cls, path_ifc): def get_path_dir(cls, path_ifc: str) -> str:
return os.path.abspath(os.path.dirname(path_ifc)) return os.path.abspath(os.path.dirname(path_ifc))
@classmethod @classmethod
def repo_from_path(cls, path): def repo_from_path(cls, path: str) -> Union[git.Repo, None]:
"""Returns a Git repository object or None""" """Returns a Git repository object or None"""
if os.path.isdir(path): if os.path.isdir(path):
@@ -72,7 +97,7 @@ class IfcGit:
return repo return repo
@classmethod @classmethod
def add_file_to_repo(cls, repo, path_file): def add_file_to_repo(cls, repo: git.Repo, path_file: str) -> None:
if os.name == "nt": if os.name == "nt":
cls.dos2unix(path_file) cls.dos2unix(path_file)
repo.index.add(path_file) repo.index.add(path_file)
@@ -80,11 +105,11 @@ class IfcGit:
bpy.ops.ifcgit.refresh() bpy.ops.ifcgit.refresh()
@classmethod @classmethod
def git_checkout(cls, path_file): def git_checkout(cls, path_file: str) -> None:
IfcGitRepo.repo.git.checkout(path_file) IfcGitRepo.repo.git.checkout(path_file)
@classmethod @classmethod
def checkout_new_branch(cls, path_file): def checkout_new_branch(cls, path_file: str) -> None:
"""Create a branch and move uncommitted changes to this branch""" """Create a branch and move uncommitted changes to this branch"""
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
if props.new_branch_name: if props.new_branch_name:
@@ -94,7 +119,7 @@ class IfcGit:
bpy.ops.ifcgit.refresh() bpy.ops.ifcgit.refresh()
@classmethod @classmethod
def git_commit(cls, path_file): def git_commit(cls, path_file: str) -> None:
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo repo = IfcGitRepo.repo
if os.name == "nt": if os.name == "nt":
@@ -104,7 +129,7 @@ class IfcGit:
props.commit_message = "" props.commit_message = ""
@classmethod @classmethod
def add_tag(cls, repo): def add_tag(cls, repo: git.Repo) -> None:
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
item = props.ifcgit_commits[props.commit_index] item = props.ifcgit_commits[props.commit_index]
repo.create_tag(props.new_tag_name, ref=item.hexsha, message=props.new_tag_message) repo.create_tag(props.new_tag_name, ref=item.hexsha, message=props.new_tag_message)
@@ -112,19 +137,19 @@ class IfcGit:
props.new_tag_message = "" props.new_tag_message = ""
@classmethod @classmethod
def delete_tag(cls, repo, tag_name): def delete_tag(cls, repo: git.Repo, tag_name: git.TagReference) -> None:
if tag_name in repo.tags: if tag_name in repo.tags:
repo.delete_tag(tag_name) repo.delete_tag(tag_name)
@classmethod @classmethod
def add_remote(cls, repo): def add_remote(cls, repo: git.Repo) -> None:
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
repo.create_remote(name=props.remote_name, url=props.remote_url) repo.create_remote(name=props.remote_name, url=props.remote_url)
props.remote_name = "" props.remote_name = ""
props.remote_url = "" props.remote_url = ""
@classmethod @classmethod
def delete_remote(cls, repo): def delete_remote(cls, repo: git.Repo) -> None:
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
remote_name = props.select_remote remote_name = props.select_remote
if remote_name in repo.remotes: if remote_name in repo.remotes:
@@ -133,7 +158,7 @@ class IfcGit:
props.select_remote = repo.remotes[0].name props.select_remote = repo.remotes[0].name
@classmethod @classmethod
def push(cls, repo, remote_name, branch_name): def push(cls, repo: git.Repo, remote_name: str, branch_name: str) -> Union[str, None]:
cls.config_push(repo) cls.config_push(repo)
remote = repo.remotes[remote_name] remote = repo.remotes[remote_name]
try: try:
@@ -142,7 +167,7 @@ class IfcGit:
return exc.stderr return exc.stderr
@classmethod @classmethod
def create_new_branch(cls): def create_new_branch(cls) -> None:
"""Convert a detached HEAD into a branch""" """Convert a detached HEAD into a branch"""
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo repo = IfcGitRepo.repo
@@ -154,7 +179,7 @@ class IfcGit:
bpy.ops.ifcgit.refresh() bpy.ops.ifcgit.refresh()
@classmethod @classmethod
def clear_commits_list(cls): def clear_commits_list(cls) -> None:
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].shading.color_type = "MATERIAL" area.spaces[0].shading.color_type = "MATERIAL"
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
@@ -163,7 +188,7 @@ class IfcGit:
props.ifcgit_commits.clear() props.ifcgit_commits.clear()
@classmethod @classmethod
def get_commits_list(cls, path_ifc, lookup): def get_commits_list(cls, path_ifc: str, lookup: dict[str, Any]) -> None:
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
repo = cls.repo_from_path(path_ifc) repo = cls.repo_from_path(path_ifc)
@@ -205,14 +230,14 @@ class IfcGit:
list_item.tags[-1].message = tag.tag.message list_item.tags[-1].message = tag.tag.message
@classmethod @classmethod
def refresh_revision_list(cls, path_ifc): def refresh_revision_list(cls, path_ifc: str) -> None:
repo = cls.repo_from_path(path_ifc) repo = cls.repo_from_path(path_ifc)
cls.clear_commits_list() cls.clear_commits_list()
lookup = cls.tags_by_hexsha(repo) lookup = cls.tags_by_hexsha(repo)
cls.get_commits_list(path_ifc, lookup) cls.get_commits_list(path_ifc, lookup)
@classmethod @classmethod
def is_valid_ref_format(cls, string): def is_valid_ref_format(cls, string: str) -> Union[re.Match[str], None]:
"""Check a bare branch or tag name is valid""" """Check a bare branch or tag name is valid"""
return re.match( return re.match(
@@ -221,7 +246,7 @@ class IfcGit:
) )
@classmethod @classmethod
def load_project(cls, path_ifc=""): def load_project(cls, path_ifc: str = "") -> None:
"""Clear and load an ifc project""" """Clear and load an ifc project"""
if path_ifc: if path_ifc:
@@ -248,7 +273,7 @@ class IfcGit:
bpy.ops.object.select_all(action="DESELECT") bpy.ops.object.select_all(action="DESELECT")
@classmethod @classmethod
def branches_by_hexsha(cls, repo): def branches_by_hexsha(cls, repo: git.Repo) -> dict[str, Any]:
"""reverse lookup for branches""" """reverse lookup for branches"""
result = {} result = {}
@@ -267,7 +292,7 @@ class IfcGit:
return result return result
@classmethod @classmethod
def tags_by_hexsha(cls, repo): def tags_by_hexsha(cls, repo: git.Repo) -> dict[str, Any]:
"""reverse lookup for tags""" """reverse lookup for tags"""
result = {} result = {}
@@ -279,7 +304,7 @@ class IfcGit:
return result return result
@classmethod @classmethod
def ifc_diff_ids(cls, repo, hash_a, hash_b, path_ifc): def ifc_diff_ids(cls, repo: git.Repo, hash_a: str, hash_b: str, path_ifc: str) -> STEP_IDS:
"""Given two revision hashes and a filename, retrieve""" """Given two revision hashes and a filename, retrieve"""
"""step-ids of modified, added and removed entities""" """step-ids of modified, added and removed entities"""
@@ -309,7 +334,7 @@ class IfcGit:
} }
@classmethod @classmethod
def get_revisions_step_ids(cls): def get_revisions_step_ids(cls) -> Union[STEP_IDS, None]:
path_ifc = bpy.data.scenes["Scene"].BIMProperties.ifc_file path_ifc = bpy.data.scenes["Scene"].BIMProperties.ifc_file
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
@@ -341,7 +366,7 @@ class IfcGit:
return step_ids return step_ids
@classmethod @classmethod
def get_modified_shape_object_step_ids(cls, step_ids): def get_modified_shape_object_step_ids(cls, step_ids: STEP_IDS) -> STEP_IDS:
model = tool.Ifc.get() model = tool.Ifc.get()
modified_shape_object_step_ids = {"modified": []} modified_shape_object_step_ids = {"modified": []}
@@ -353,7 +378,7 @@ class IfcGit:
return modified_shape_object_step_ids return modified_shape_object_step_ids
@classmethod @classmethod
def update_step_ids(cls, step_ids, modified_shape_object_step_ids): def update_step_ids(cls, step_ids: STEP_IDS, modified_shape_object_step_ids: STEP_IDS) -> STEP_IDS:
final_step_ids = {} final_step_ids = {}
final_step_ids["added"] = step_ids["added"] final_step_ids["added"] = step_ids["added"]
@@ -362,7 +387,7 @@ class IfcGit:
return final_step_ids return final_step_ids
@classmethod @classmethod
def colourise(cls, step_ids): def colourise(cls, step_ids: STEP_IDS) -> None:
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].shading.color_type = "OBJECT" area.spaces[0].shading.color_type = "OBJECT"
bpy.ops.object.select_all(action="DESELECT") bpy.ops.object.select_all(action="DESELECT")
@@ -384,7 +409,7 @@ class IfcGit:
obj.color = (1.0, 1.0, 1.0, 0.5) obj.color = (1.0, 1.0, 1.0, 0.5)
@classmethod @classmethod
def switch_to_revision_item(cls): def switch_to_revision_item(cls) -> None:
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index] item = props.ifcgit_commits[props.commit_index]
@@ -399,13 +424,13 @@ class IfcGit:
repo.git.checkout(item.hexsha) repo.git.checkout(item.hexsha)
@classmethod @classmethod
def delete_collection(cls, blender_collection): def delete_collection(cls, blender_collection: bpy.types.Collection) -> None:
for obj in blender_collection.objects: for obj in blender_collection.objects:
bpy.data.objects.remove(obj, do_unlink=True) bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(blender_collection) bpy.data.collections.remove(blender_collection)
@classmethod @classmethod
def is_valid_branch_name(cls, new_branch_name): def is_valid_branch_name(cls, new_branch_name: str):
"""Check if a branch name is valid and doesn't conflict with existing branches""" """Check if a branch name is valid and doesn't conflict with existing branches"""
if not cls.is_valid_ref_format(new_branch_name): if not cls.is_valid_ref_format(new_branch_name):
return False return False
@@ -414,7 +439,7 @@ class IfcGit:
return True return True
@classmethod @classmethod
def config_ifcmerge(cls): def config_ifcmerge(cls) -> None:
config_reader = IfcGitRepo.repo.config_reader() config_reader = IfcGitRepo.repo.config_reader()
section = 'mergetool "ifcmerge"' section = 'mergetool "ifcmerge"'
if not config_reader.has_section(section): if not config_reader.has_section(section):
@@ -428,7 +453,7 @@ class IfcGit:
config_writer.set_value(section, "trustExitCode", True) config_writer.set_value(section, "trustExitCode", True)
@classmethod @classmethod
def config_push(cls, repo): def config_push(cls, repo: git.Repo) -> None:
"""Set push.autoSetupRemote""" """Set push.autoSetupRemote"""
config_reader = repo.config_reader() config_reader = repo.config_reader()
if not config_reader.has_section("push"): if not config_reader.has_section("push"):
@@ -437,7 +462,7 @@ class IfcGit:
config_writer.set_value("push", "autoSetupRemote", True) config_writer.set_value("push", "autoSetupRemote", True)
@classmethod @classmethod
def config_info_attributes(cls, repo): def config_info_attributes(cls, repo: git.Repo) -> None:
"""Set IFC files as text in .git/info/attributes""" """Set IFC files as text in .git/info/attributes"""
path_attributes = os.path.join(repo.git_dir, "info", "attributes") path_attributes = os.path.join(repo.git_dir, "info", "attributes")
if not os.path.exists(path_attributes): if not os.path.exists(path_attributes):
@@ -446,7 +471,7 @@ class IfcGit:
f.write("*.ifc text") f.write("*.ifc text")
@classmethod @classmethod
def dos2unix(cls, path_file): def dos2unix(cls, path_file: str) -> None:
with open(path_file, "rb") as infile: with open(path_file, "rb") as infile:
content = infile.read() content = infile.read()
with open(path_file, "wb") as output: with open(path_file, "wb") as output:
@@ -454,7 +479,7 @@ class IfcGit:
output.write(line + b"\n") output.write(line + b"\n")
@classmethod @classmethod
def execute_merge(cls, path_ifc, operator): def execute_merge(cls, path_ifc: str, operator: bpy.types.Operator) -> Union[None, False]:
props = bpy.context.scene.IfcGitProperties props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index] item = props.ifcgit_commits[props.commit_index]
@@ -498,7 +523,7 @@ class IfcGit:
cls.refresh_revision_list(path_ifc) cls.refresh_revision_list(path_ifc)
@classmethod @classmethod
def entity_log(cls, path_ifc, step_id): def entity_log(cls, path_ifc: str, step_id: int) -> str:
"""Raw git log for this entity""" """Raw git log for this entity"""
repo = IfcGitRepo.repo repo = IfcGitRepo.repo
if not repo: if not repo:
@@ -514,4 +539,4 @@ class IfcGit:
class IfcGitRepo: class IfcGitRepo:
repo = None repo: git.Repo = None
+1 -1
View File
@@ -695,7 +695,7 @@ class Loader(blenderbim.core.tool.Loader):
cls.settings.false_origin = ifcopenshell.util.geolocation.auto_xyz2enh( cls.settings.false_origin = ifcopenshell.util.geolocation.auto_xyz2enh(
ifc_file, *offset_point, should_return_in_map_units=False ifc_file, *offset_point, should_return_in_map_units=False
) )
if angle := ifcopenshell.util.geolocation.get_grid_north(ifc_file): if (angle := ifcopenshell.util.geolocation.get_grid_north(ifc_file)) and not tool.Cad.is_x(angle, 0):
cls.settings.project_north = angle cls.settings.project_north = angle
cls.set_manual_blender_offset(ifc_file) cls.set_manual_blender_offset(ifc_file)
+53 -32
View File
@@ -114,10 +114,10 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod @classmethod
def run_spatial_assign_container( def run_spatial_assign_container(
cls, structure_obj: bpy.types.Object, element_obj: bpy.types.Object cls, container: ifcopenshell.entity_instance, element_obj: bpy.types.Object
) -> Union[ifcopenshell.entity_instance, None]: ) -> Union[ifcopenshell.entity_instance, None]:
return blenderbim.core.spatial.assign_container( return blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=structure_obj, element_obj=element_obj tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj
) )
@classmethod @classmethod
@@ -392,10 +392,11 @@ class Spatial(blenderbim.core.tool.Spatial):
continue continue
old_mesh = obj.data old_mesh = obj.data
assert isinstance(old_mesh, bpy.types.Mesh)
if visible_element.HasOpenings: if visible_element.HasOpenings:
new_mesh = cls.get_gross_mesh_from_element(visible_element) new_mesh = cls.get_gross_mesh_from_element(visible_element)
else: else:
new_mesh = obj.data.copy() new_mesh = old_mesh.copy()
obj.data = new_mesh obj.data = new_mesh
# Boundary objects are likely triangulated. If a triangulated quad # Boundary objects are likely triangulated. If a triangulated quad
@@ -463,15 +464,15 @@ class Spatial(blenderbim.core.tool.Spatial):
return mesh return mesh
@classmethod @classmethod
def get_x_y_z_h_mat_from_active_obj(cls, active_obj: bpy.types.Object) -> tuple[float, float, float, float, Matrix]: def get_x_y_z_h_mat_from_obj(cls, obj: bpy.types.Object) -> tuple[float, float, float, float, Matrix]:
mat = active_obj.matrix_world mat = obj.matrix_world
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector()) local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center global_bbox_center = mat @ local_bbox_center
x = global_bbox_center.x x = global_bbox_center.x
y = global_bbox_center.y y = global_bbox_center.y
z = (mat @ Vector(active_obj.bound_box[0])).z z = (mat @ Vector(obj.bound_box[0])).z
h = active_obj.dimensions.z h = obj.dimensions.z
return x, y, z, h, mat return x, y, z, h, mat
@classmethod @classmethod
@@ -489,9 +490,15 @@ class Spatial(blenderbim.core.tool.Spatial):
selected_objects = bpy.context.selected_objects selected_objects = bpy.context.selected_objects
boundary_elements = cls.get_boundary_elements(selected_objects) boundary_elements = cls.get_boundary_elements(selected_objects)
polys = cls.get_polygons(boundary_elements) polys = cls.get_polygons(boundary_elements)
converted_tolerance = cls.get_converted_tolerance(tolerance=0.03) converted_tolerance = cls.get_converted_tolerance(tolerance_si=0.03)
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2) union = shapely.ops.unary_union(polys).buffer(
union = cls.get_purged_inner_holes_poly(union_geom=union, min_area=cls.get_converted_tolerance(tolerance=0.1)) converted_tolerance,
cap_style=shapely.constructive.BufferCapStyle.flat,
join_style=shapely.constructive.BufferJoinStyle.mitre,
)
union = cls.get_purged_inner_holes_poly(
union_geom=union, min_area=cls.get_converted_tolerance(tolerance_si=0.1)
)
return union return union
@classmethod @classmethod
@@ -521,28 +528,20 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod @classmethod
def get_obj_base_points(cls, obj: bpy.types.Object) -> dict[str, tuple[float, float]]: def get_obj_base_points(cls, obj: bpy.types.Object) -> dict[str, tuple[float, float]]:
x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box] si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box] bbox_ws = [obj.matrix_world @ Vector(v) / si_conversion for v in obj.bound_box]
return { return {
"low_left": (x_values[0], y_values[0]), "low_left": (bbox_ws[0].x, bbox_ws[0].y),
"high_left": (x_values[3], y_values[3]), "high_left": (bbox_ws[3].x, bbox_ws[3].y),
"low_right": (x_values[4], y_values[4]), "low_right": (bbox_ws[4].x, bbox_ws[4].y),
"high_right": (x_values[7], y_values[7]), "high_right": (bbox_ws[7].x, bbox_ws[7].y),
} }
@classmethod @classmethod
def get_converted_tolerance(cls, tolerance: float) -> float: def get_converted_tolerance(cls, tolerance_si: float) -> float:
model = tool.Ifc.get() model = tool.Ifc.get()
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT") si_conversion = ifcopenshell.util.unit.calculate_unit_scale(model)
prefix = getattr(project_unit, "Prefix", None) return tolerance_si / si_conversion
return ifcopenshell.util.unit.convert(
value=tolerance,
from_prefix=None,
from_unit="METRE",
to_prefix=prefix,
to_unit=project_unit.Name,
)
@classmethod @classmethod
def get_purged_inner_holes_poly(cls, union_geom: Polygon, min_area: float) -> Polygon: def get_purged_inner_holes_poly(cls, union_geom: Polygon, min_area: float) -> Polygon:
@@ -575,8 +574,13 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod @classmethod
def get_buffered_poly_from_linear_ring(cls, linear_ring: shapely.LinearRing) -> Polygon: def get_buffered_poly_from_linear_ring(cls, linear_ring: shapely.LinearRing) -> Polygon:
poly = Polygon(linear_ring) poly = Polygon(linear_ring)
converted_tolerance = cls.get_converted_tolerance(tolerance=0.03) converted_tolerance = cls.get_converted_tolerance(tolerance_si=0.03)
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style=2, join_style=2) poly = poly.buffer(
converted_tolerance,
single_sided=True,
cap_style=shapely.BufferCapStyle.flat,
join_style=shapely.BufferJoinStyle.mitre,
)
return poly return poly
@classmethod @classmethod
@@ -587,8 +591,10 @@ class Spatial(blenderbim.core.tool.Spatial):
bm.edges.index_update() bm.edges.index_update()
mat_invert = mat.inverted() mat_invert = mat.inverted()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]] new_verts = [
bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion)) for v in poly.exterior.coords[0:-1]
]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
@@ -866,3 +872,18 @@ class Spatial(blenderbim.core.tool.Spatial):
if (element := tool.Ifc.get_entity(obj)) and tool.Root.is_spatial_element(element): if (element := tool.Ifc.get_entity(obj)) and tool.Root.is_spatial_element(element):
results.append(element) results.append(element)
return results return results
@classmethod
def get_selected_objects_without_containers(cls) -> list[bpy.types.Object]:
"""Get selected objects skipping spatial elements.
Useful for operators that are using selected objects to identify selected containers.
Note that those operators are typically have a limitation since they can't tell
objects to operate on from containers that should be used in the operation.
E.g. we cannot bim.copy_to_container containers to other containers."""
results: list[bpy.types.Object] = []
for obj in tool.Blender.get_selected_objects():
if (element := tool.Ifc.get_entity(obj)) and not tool.Root.is_spatial_element(element):
results.append(obj)
return results
-4
View File
@@ -99,10 +99,6 @@ class Type(blenderbim.core.tool.Type):
return "Usage" in material.is_a() return "Usage" in material.is_a()
return False return False
@classmethod
def remove_object(cls, obj: bpy.types.Object) -> None:
bpy.data.objects.remove(obj)
@classmethod @classmethod
def run_geometry_add_representation( def run_geometry_add_representation(
cls, cls,
+48 -17
View File
@@ -22,6 +22,7 @@ import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import ifcopenshell.api.sequence import ifcopenshell.api.sequence
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import time
import socket import socket
import sys import sys
import os import os
@@ -122,7 +123,8 @@ class Web(blenderbim.core.tool.Web):
blenderbim_lib_path = os.path.join(blenderbim_path, "libs", "site", "packages") blenderbim_lib_path = os.path.join(blenderbim_path, "libs", "site", "packages")
env = os.environ.copy() env = os.environ.copy()
env["blenderbim_lib_path"] = str(blenderbim_lib_path) env["BLENDERBIM_LIB_PATH"] = str(blenderbim_lib_path)
env["BLENDERBIM_VERSION"] = tool.Blender.get_blenderbim_version()
ws_process = subprocess.Popen( ws_process = subprocess.Popen(
[sys.executable, ws_path, "--p", str(port), "--host", "127.0.0.1"], [sys.executable, ws_path, "--p", str(port), "--host", "127.0.0.1"],
@@ -130,19 +132,6 @@ class Web(blenderbim.core.tool.Web):
env=env, env=env,
) )
pid_file = os.path.join(webui_path, "running_pid.json")
if os.path.exists(pid_file):
with open(pid_file, "r") as f:
pids = json.load(f)
else:
pids = {}
pids[str(ws_process.pid)] = port
with open(pid_file, "w") as f:
json.dump(pids, f, indent=4)
cls.set_is_running(True) cls.set_is_running(True)
@classmethod @classmethod
@@ -223,12 +212,29 @@ class Web(blenderbim.core.tool.Web):
with open(pid_file, "w") as f: with open(pid_file, "w") as f:
json.dump(pids, f, indent=4) json.dump(pids, f, indent=4)
ws_process.terminate() ws_process.kill()
ws_process.wait()
ws_process = None ws_process = None
cls.set_is_running(False) cls.set_is_running(False)
print("Websocket server terminated successfully") print("Websocket server killed successfully")
@classmethod
def has_started(cls, port):
max_time = 5
start = time.time()
while True:
if time.time() - start > max_time:
return False
webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui")
pid_file = os.path.join(webui_path, "running_pid.json")
try:
with open(pid_file, "r") as f:
data = json.load(f)
if port in data.values():
return True
except:
pass
time.sleep(0.1)
@classmethod @classmethod
def send_webui_data( def send_webui_data(
@@ -279,6 +285,8 @@ class Web(blenderbim.core.tool.Web):
cls.handle_csv_operator(operator["operator"]) cls.handle_csv_operator(operator["operator"])
elif operator["sourcePage"] == "gantt": elif operator["sourcePage"] == "gantt":
cls.handle_gantt_operator(operator["operator"]) cls.handle_gantt_operator(operator["operator"])
elif operator["sourcePage"] == "drawings":
cls.handle_drawings_operator(operator["operator"])
return 1.0 return 1.0
@classmethod @classmethod
@@ -323,6 +331,29 @@ class Web(blenderbim.core.tool.Web):
gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)} gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
cls.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data") cls.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
@classmethod
def handle_drawings_operator(cls, operator_data: dict) -> None:
if operator_data["type"] == "getDrawings":
drawings_data = []
sheets_data = []
ifc_file_dir = os.path.dirname(bpy.context.scene.BIMProperties.ifc_file)
sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"]
for sheet in sorted(sheets, key=lambda s: getattr(s, "Identification", getattr(s, "DocumentId", None))):
for reference in tool.Drawing.get_document_references(sheet):
reference_description = tool.Drawing.get_reference_description(reference)
reference_location = tool.Drawing.get_reference_location(reference)
reference_name = os.path.basename(reference_location)
reference_path = os.path.join(ifc_file_dir, reference_location)
if reference_description == "SHEET":
sheets_data.append({"name": reference_name, "path": reference_path})
if reference_description == "DRAWING":
drawings_data.append({"name": reference_name, "path": reference_path})
cls.send_webui_data(
data={"drawings": drawings_data, "sheets": sheets_data}, data_key="drawings_data", event="drawings_data"
)
@classmethod @classmethod
def open_web_browser(cls, port: int) -> None: def open_web_browser(cls, port: int) -> None:
webbrowser.open(f"http://127.0.0.1:{port}/") webbrowser.open(f"http://127.0.0.1:{port}/")
+1 -4
View File
@@ -150,7 +150,6 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
ifcopenshell ifcopenshell
bcf bcf
ifcclash ifcclash
bimtester
ifccobie ifccobie
ifccsv ifccsv
ifcdiff ifcdiff
@@ -172,7 +171,6 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
elementpath elementpath
six six
lark-parser lark-parser
behave
parse parse
parse_type parse_type
xlsxwriter xlsxwriter
@@ -184,8 +182,7 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
Notes: Notes:
1. ``ifcopenshell`` almost always requires the latest version due to the fast paced nature of the add-on development. 1. ``ifcopenshell`` almost always requires the latest version due to the fast paced nature of the add-on development.
2. ``behave`` requires `patches <https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.8.0/src/ifcbimtester/patch>`__. 2. ``ifcjson`` can be found `here <https://github.com/IFCJSON-Team/IFC2JSON_python/tree/master/file_converters>`__.
3. ``ifcjson`` can be found `here <https://github.com/IFCJSON-Team/IFC2JSON_python/tree/master/file_converters>`__.
Required static assets are: Required static assets are:
:: ::
-1
View File
@@ -2,7 +2,6 @@
markers = markers =
aggregate aggregate
attribute attribute
bimtester
brick brick
classification classification
context context
@@ -22,9 +22,7 @@ Scenario: Assign object
And I press "bim.enable_editing_aggregate" And I press "bim.enable_editing_aggregate"
And the variable "relating_object" is "tool.Ifc.get().by_type('IfcSite')[0].id()" And the variable "relating_object" is "tool.Ifc.get().by_type('IfcSite')[0].id()"
When I press "bim.aggregate_assign_object(relating_object={relating_object})" When I press "bim.aggregate_assign_object(relating_object={relating_object})"
Then the object "IfcSite/My Site" is in the collection "IfcSite/My Site" Then the object "IfcBuildingStorey/My Storey" is aggregated by object "IfcSite/My Site"
And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey"
And the collection "IfcBuildingStorey/My Storey" is in the collection "IfcSite/My Site"
Scenario: Unassign object Scenario: Unassign object
Given an empty IFC project Given an empty IFC project
@@ -34,29 +32,7 @@ Scenario: Unassign object
And I press "bim.aggregate_assign_object(relating_object={relating_object})" And I press "bim.aggregate_assign_object(relating_object={relating_object})"
And the object "IfcBuildingStorey/My Storey" is selected And the object "IfcBuildingStorey/My Storey" is selected
When I press "bim.aggregate_unassign_object" When I press "bim.aggregate_unassign_object"
Then the object "IfcSite/My Site" is in the collection "IfcSite/My Site" Then the object "IfcBuildingStorey/My Storey" has no aggregate
And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey"
And the collection "IfcBuildingStorey/My Storey" is in the collection "IfcProject/My Project"
Scenario: Unassign object - multiple objects are contained again to their indirect container
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcMember"
And I press "bim.assign_class"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_class" to "IfcCovering"
And I press "bim.assign_class"
And the object "IfcMember/Cube" is selected
And additionally the object "IfcCovering/Cube" is selected
And I press "bim.add_aggregate(ifc_class='IfcWall')"
And the object "IfcMember/Cube" is selected
And additionally the object "IfcCovering/Cube" is selected
When I press "bim.aggregate_unassign_object"
Then the object "IfcMember/Cube" is in the collection "IfcBuildingStorey/My Storey"
And the object "IfcCovering/Cube" is in the collection "IfcBuildingStorey/My Storey"
Scenario: Add aggregate Scenario: Add aggregate
Given an empty IFC project Given an empty IFC project
@@ -67,23 +43,9 @@ Scenario: Add aggregate
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate" When I press "bim.add_aggregate"
Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly" Then the object "IfcElementAssembly/Default_Name" exists
And the object "IfcElementAssembly/Assembly" is in the collection "IfcElementAssembly/Assembly" And the object "IfcWall/Cube" is aggregated by object "IfcElementAssembly/Default_Name"
And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
Scenario: Add aggregate - with the aggregate inheriting the existing spatial collection
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey"
And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate"
Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly"
And the object "IfcElementAssembly/Assembly" is in the collection "IfcElementAssembly/Assembly"
And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey"
Scenario: Add aggregate - add a nested aggregate Scenario: Add aggregate - add a nested aggregate
Given an empty IFC project Given an empty IFC project
@@ -92,15 +54,15 @@ Scenario: Add aggregate - add a nested aggregate
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate" When I press "bim.add_aggregate"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.add_aggregate" And I press "bim.add_aggregate(aggregate_name='Default_Name2')"
Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly.001" Then the object "IfcElementAssembly/Default_Name" exists
And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001" And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly" And the object "IfcElementAssembly/Default_Name2" exists
And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" And the object "IfcWall/Cube" is aggregated by object "IfcElementAssembly/Default_Name2"
And the object "IfcElementAssembly/Default_Name2" is aggregated by object "IfcElementAssembly/Default_Name"
Scenario: Add aggregate - add multiple elements to a custom aggregate class Scenario: Add aggregate - add multiple elements to a custom aggregate class
Given an empty IFC project Given an empty IFC project
@@ -115,8 +77,7 @@ Scenario: Add aggregate - add multiple elements to a custom aggregate class
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcMember/Cube" is selected And the object "IfcMember/Cube" is selected
And additionally the object "IfcCovering/Cube" is selected And additionally the object "IfcCovering/Cube" is selected
When I press "bim.add_aggregate(ifc_class='IfcWall')" When I press "bim.add_aggregate(ifc_class='IfcWall', aggregate_name='Assembly')"
Then the object "IfcMember/Cube" is in the collection "IfcWall/Assembly" Then the object "IfcMember/Cube" is aggregated by object "IfcWall/Assembly"
And the object "IfcCovering/Cube" is in the collection "IfcWall/Assembly" And the object "IfcCovering/Cube" is aggregated by object "IfcWall/Assembly"
And the object "IfcWall/Assembly" is in the collection "IfcWall/Assembly" And the object "IfcWall/Assembly" is contained in object "IfcBuildingStorey/My Storey"
And the collection "IfcWall/Assembly" is in the collection "IfcBuildingStorey/My Storey"
@@ -4,34 +4,34 @@ Feature: Attribute
Scenario: Enable editing attributes Scenario: Enable editing attributes
Given an empty IFC project Given an empty IFC project
And the object "IfcSite/My Site" is selected And the object "IfcSite/My Site" is selected
When I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" When I press "bim.enable_editing_attributes(obj='IfcSite/My Site')"
Then nothing happens Then nothing happens
Scenario: Disable editing attributes Scenario: Disable editing attributes
Given an empty IFC project Given an empty IFC project
And the object "IfcSite/My Site" is selected And the object "IfcSite/My Site" is selected
And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')"
When I press "bim.disable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" When I press "bim.disable_editing_attributes(obj='IfcSite/My Site')"
Then nothing happens Then nothing happens
Scenario: Edit attributes Scenario: Edit attributes
Given an empty IFC project Given an empty IFC project
And the object "IfcSite/My Site" is selected And the object "IfcSite/My Site" is selected
And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')"
And I set "active_object.BIMAttributeProperties.attributes[1].string_value" to "Name" And I set "active_object.BIMAttributeProperties.attributes[1].string_value" to "Name"
When I press "bim.edit_attributes(obj='IfcSite/My Site', obj_type='Object')" When I press "bim.edit_attributes(obj='IfcSite/My Site')"
Then the object "IfcSite/Name" is an "IfcSite" Then the object "IfcSite/Name" is an "IfcSite"
And the object "IfcSite/My Site" does not exist And the object "IfcSite/My Site" does not exist
Scenario: Edit attributes - longitude / latitude Scenario: Edit attributes - longitude / latitude
Given an empty IFC project Given an empty IFC project
And the object "IfcSite/My Site" is selected And the object "IfcSite/My Site" is selected
And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')"
Then "active_object.BIMAttributeProperties.attributes[6].name" is "RefLatitude" Then "active_object.BIMAttributeProperties.attributes[6].name" is "RefLatitude"
When I set "active_object.BIMAttributeProperties.attributes[6].string_value" to "[1,2]" When I set "active_object.BIMAttributeProperties.attributes[6].string_value" to "[1,2]"
And I press "bim.edit_attributes(obj='IfcSite/My Site', obj_type='Object')" And I press "bim.edit_attributes(obj='IfcSite/My Site')"
And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')"
And I press "bim.edit_attributes(obj='IfcSite/My Site', obj_type='Object')" And I press "bim.edit_attributes(obj='IfcSite/My Site')"
Then nothing happens Then nothing happens
Scenario: Copy attribute to selected Scenario: Copy attribute to selected
@@ -48,7 +48,7 @@ Scenario: Copy attribute to selected
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And additionally the object "IfcWall/Cube.001" is selected And additionally the object "IfcWall/Cube.001" is selected
And I press "bim.enable_editing_attributes(obj='IfcWall/Cube.001', obj_type='Object')" And I press "bim.enable_editing_attributes(obj='IfcWall/Cube.001')"
And I set "active_object.BIMAttributeProperties.attributes[2].string_value" to "Foo" And I set "active_object.BIMAttributeProperties.attributes[2].string_value" to "Foo"
When I press "bim.copy_attribute_to_selection(name='Description')" When I press "bim.copy_attribute_to_selection(name='Description')"
Then nothing happens Then nothing happens
@@ -1,9 +0,0 @@
@bimtester
Feature: Bimtester
Scenario: Execute Bimtester
Given an empty IFC project
When I set "scene.BimTesterProperties.should_load_from_memory" to "True"
And I set "scene.BimTesterProperties.feature" to "{cwd}/test/files/sample.feature"
And I press "bim.execute_bim_tester"
Then nothing happens
+1 -1
View File
@@ -377,7 +377,7 @@ Scenario: Assign cost item quantity - quantity based
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')" And I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')"
And I press "bim.calculate_all_quantities" And I press "bim.perform_quantity_take_off"
When I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='NetVolume')" When I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='NetVolume')"
Then nothing happens Then nothing happens
@@ -8,8 +8,36 @@ Scenario: Execute generate flooring coverings from walls
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
# 1st wall
And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance"
And the object "IfcWall/Wall" is selected And the object "IfcWall/Wall" is selected
When I press "bim.add_instance_flooring_coverings_from_walls" And I press "bim.change_layer_length(length=3.6)"
Then nothing happens # 2nd wall
And the cursor is at "3.6,0.1,3"
And I set "scene.BIMModelProperties.length" to "2.0"
And I press "bim.add_constr_type_instance"
# 3rd wall
And the cursor is at "3.5,2.1,3"
And I set "scene.BIMModelProperties.length" to "3.5"
And I press "bim.add_constr_type_instance"
# 4th wall
And the cursor is at "0,2.0,0"
And I set "scene.BIMModelProperties.length" to "1.9"
And I press "bim.add_constr_type_instance"
# add_instance_flooring_coverings_from_walls is expecting FLOORING predefined type.
And the object "IfcCoveringType/COV30" is selected
And I press "bim.enable_editing_attributes(obj='IfcCoveringType/COV30')"
And I set "active_object.BIMAttributeProperties.attributes[6].enum_value" to "FLOORING"
And I press "bim.edit_attributes(obj='IfcCoveringType/COV30')"
# Run the operator.
When the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall.002" is selected
And additionally the object "IfcWall/Wall.003" is selected
And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_instance_flooring_coverings_from_walls"
Then the object "IfcCovering/Covering0" exists
And the object "IfcCovering/Covering0" is at "1.8,1.05,0.0"
And the object "IfcCovering/Covering0" dimensions are "3.4,1.9,0.03"
@@ -93,7 +93,6 @@ Scenario: Remove drawing - deleting active drawing
And the collection "IfcAnnotation/PLAN_VIEW" exists And the collection "IfcAnnotation/PLAN_VIEW" exists
And I set "scene.DocProperties.active_drawing_index" to "0" And I set "scene.DocProperties.active_drawing_index" to "0"
And I press "bim.activate_drawing(drawing={drawing})" And I press "bim.activate_drawing(drawing={drawing})"
And the object "IfcAnnotation/PLAN_VIEW" is selected When the object "IfcAnnotation/PLAN_VIEW" is selected
When I press "bim.override_object_delete" And I press "bim.override_object_delete"
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
@@ -93,7 +93,7 @@ Scenario: Switch representation - current edited representation is updated prior
And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)" And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)"
And the variable "representation" is "[r for r in {ifc}.by_type('IfcShapeRepresentation') if r.RepresentationType=='Annotation2D'][0].id()" And the variable "representation" is "[r for r in {ifc}.by_type('IfcShapeRepresentation') if r.RepresentationType=='Annotation2D'][0].id()"
And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)" And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)"
When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcWall/Cube" dimensions are "4,4,0" Then the object "IfcWall/Cube" dimensions are "4,4,0"
Scenario: Switch representation - current edited representation is discarded if switching to a box Scenario: Switch representation - current edited representation is discarded if switching to a box
@@ -108,7 +108,7 @@ Scenario: Switch representation - current edited representation is discarded if
And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)"
And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)"
When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcWall/Cube" dimensions are "2,2,2" Then the object "IfcWall/Cube" dimensions are "2,2,2"
Scenario: Switch representation - existing Blender modifiers must be purged Scenario: Switch representation - existing Blender modifiers must be purged
@@ -131,8 +131,10 @@ Scenario: Remove representation - remove an active representation
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" When the variable "representation_body" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
And I press "bim.remove_representation(representation_id={representation})" And the variable "representation_bbox" is "{ifc}.by_type('IfcShapeRepresentation')[1].id()"
And I press "bim.remove_representation(representation_id={representation_body})"
And I press "bim.remove_representation(representation_id={representation_bbox})"
Then the object "IfcWall/Cube" has no data Then the object "IfcWall/Cube" has no data
Scenario: Remove representation - remove an unloaded representation Scenario: Remove representation - remove an unloaded representation
@@ -160,8 +162,10 @@ Scenario: Remove representation - remove an instanced representation from an act
And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance"
And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance"
And the object "IfcWallType/Cube" is selected And the object "IfcWallType/Cube" is selected
When the variable "representation" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()" When the variable "representation_body" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()"
And I press "bim.remove_representation(representation_id={representation})" And the variable "representation_bbox" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[0].MappedRepresentation.id()"
And I press "bim.remove_representation(representation_id={representation_body})"
And I press "bim.remove_representation(representation_id={representation_bbox})"
Then the object "IfcWallType/Cube" has no data Then the object "IfcWallType/Cube" has no data
Then the object "IfcWall/Wall" has no data Then the object "IfcWall/Wall" has no data
Then the object "IfcWall/Wall.001" has no data Then the object "IfcWall/Wall.001" has no data
@@ -179,8 +183,10 @@ Scenario: Remove representation - remove an instanced representation from an act
And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance"
And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance"
And the object "IfcWall/Wall" is selected And the object "IfcWall/Wall" is selected
When the variable "representation" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()" When the variable "representation_body" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()"
And I press "bim.remove_representation(representation_id={representation})" And the variable "representation_bbox" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[0].id()"
And I press "bim.remove_representation(representation_id={representation_body})"
And I press "bim.remove_representation(representation_id={representation_bbox})"
Then the object "IfcWallType/Cube" has no data Then the object "IfcWallType/Cube" has no data
Then the object "IfcWall/Wall" has no data Then the object "IfcWall/Wall" has no data
Then the object "IfcWall/Wall.001" has no data Then the object "IfcWall/Wall.001" has no data
@@ -339,7 +345,7 @@ Scenario: Override duplicate move - copying a coloured representation
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
And I press "bim.assign_style_to_selected(style_id={style})" And I press "bim.assign_style_to_selected(style_id={style})"
When I duplicate the selected objects When I duplicate the selected objects
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
And an empty Blender session is started And an empty Blender session is started
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)"
Then the material "Style" colour is "1,0,0,1" Then the material "Style" colour is "1,0,0,1"
@@ -427,18 +433,14 @@ Scenario: Override duplicate move - copying an aggregate
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate" When I press "bim.add_aggregate(aggregate_name='Assembly')"
Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly"
And the object "IfcElementAssembly/Assembly" is in the collection "IfcElementAssembly/Assembly"
And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey"
When the object "IfcWall/Cube" is selected When the object "IfcWall/Cube" is selected
And additionally the object "IfcElementAssembly/Assembly" is selected And additionally the object "IfcElementAssembly/Assembly" is selected
When I duplicate the selected objects When I duplicate the selected objects
Then the object "IfcWall/Cube.001" exists Then the object "IfcWall/Cube.001" exists
And the object "IfcWall/Cube.001" is in the collection "IfcElementAssembly/Assembly.001"
And the object "IfcElementAssembly/Assembly.001" exists And the object "IfcElementAssembly/Assembly.001" exists
And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001" And the object "IfcWall/Cube.001" is aggregated by object "IfcElementAssembly/Assembly.001"
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey" And the object "IfcElementAssembly/Assembly.001" is contained in object "IfcBuildingStorey/My Storey"
Scenario: Override duplicate move - copying objects with connection Scenario: Override duplicate move - copying objects with connection
Given an empty IFC project Given an empty IFC project
@@ -451,7 +453,7 @@ Scenario: Override duplicate move - copying objects with connection
And the object "IfcWall/Wall" dimensions are "1,0.1,3" And the object "IfcWall/Wall" dimensions are "1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0,0,0" And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
When I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" When I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
When I press "bim.add_constr_type_instance" When I press "bim.add_constr_type_instance"
Then the object "IfcSlab/Slab" is an "IfcSlab" Then the object "IfcSlab/Slab" is an "IfcSlab"
@@ -607,4 +609,3 @@ Scenario: Refresh linked aggregate - after duplicating an object
Then the object "IfcWall/Wall_01.001" exists Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists And the object "IfcWall/Wall_02.001" exists
And the object "IfcWall/Wall_03.001" exists And the object "IfcWall/Wall_03.001" exists
@@ -157,7 +157,7 @@ Scenario: Unassign material - removing inherited material
And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')"
Then the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default" Then the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default"
When I press "bim.unassign_material" When I press "bim.unassign_material"
Then the object "IfcWall/Cube" has no IFC materials Then the object "IfcWall/Cube" has no IFC materials
+3 -2
View File
@@ -33,7 +33,8 @@ Scenario: Resize to storey
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()" And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
And I press "bim.assign_container(structure={storey})" And I press "bim.set_default_container(container={storey})"
And I press "bim.assign_container()"
When I press "bim.resize_to_storey(total_storeys=1)" When I press "bim.resize_to_storey(total_storeys=1)"
Then nothing happens Then nothing happens
@@ -56,4 +57,4 @@ Scenario: Enabling and disabling IFC Sverchok
And I press "preferences.addon_enable(module="sverchok")" And I press "preferences.addon_enable(module="sverchok")"
And I press "preferences.addon_enable(module="ifcsverchok")" And I press "preferences.addon_enable(module="ifcsverchok")"
And I press "preferences.addon_disable(module="sverchok")" And I press "preferences.addon_disable(module="sverchok")"
And I press "preferences.addon_disable(module="ifcsverchok")" And I press "preferences.addon_disable(module="ifcsverchok")"
+4 -12
View File
@@ -66,14 +66,6 @@ Scenario: Add grid
And the object "IfcGridAxis/02" is an "IfcGridAxis" And the object "IfcGridAxis/02" is an "IfcGridAxis"
And the object "IfcGridAxis/03" is an "IfcGridAxis" And the object "IfcGridAxis/03" is an "IfcGridAxis"
Scenario: Pie update container
Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
Then the object "IfcSlab/Slab" is in the collection "IfcBuildingStorey/Ground Floor"
When the object "IfcSlab/Slab" is placed in the collection "IfcBuildingStorey/Level 1"
And I press "bim.pie_update_container"
Then nothing happens
Scenario: Add a wall Scenario: Add a wall
Given an empty IFC project Given an empty IFC project
And I load the demo construction library And I load the demo construction library
@@ -272,7 +264,7 @@ Scenario: Add a slab
Given an empty IFC project Given an empty IFC project
And I load the demo construction library And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
When I press "bim.hotkey(hotkey='S_A')" When I press "bim.hotkey(hotkey='S_A')"
Then the object "IfcSlab/Slab" is an "IfcSlab" Then the object "IfcSlab/Slab" is an "IfcSlab"
@@ -284,7 +276,7 @@ Scenario: Enable editing a slab profile
Given an empty IFC project Given an empty IFC project
And I load the demo construction library And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.hotkey(hotkey='S_A')" And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcSlab/Slab" is selected And the object "IfcSlab/Slab" is selected
@@ -297,7 +289,7 @@ Scenario: Disable editing a slab profile
Given an empty IFC project Given an empty IFC project
And I load the demo construction library And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.hotkey(hotkey='S_A')" And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcSlab/Slab" is selected And the object "IfcSlab/Slab" is selected
@@ -311,7 +303,7 @@ Scenario: Edit a slab profile
Given an empty IFC project Given an empty IFC project
And I load the demo construction library And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.hotkey(hotkey='S_A')" And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcSlab/Slab" is selected And the object "IfcSlab/Slab" is selected
+10 -10
View File
@@ -833,31 +833,31 @@ Scenario: Unlink IFC
Scenario: Export IFC - blank project Scenario: Export IFC - blank project
Given an empty IFC project Given an empty IFC project
When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then nothing happens Then nothing happens
Scenario: Export IFC - with basic contents Scenario: Export IFC - with basic contents
Given an empty Blender session Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/temp/export.ifc" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/temp/export.ifc"
Scenario: Export IFC - with basic contents and saving as another file Scenario: Export IFC - with basic contents and saving as another file
Given an empty Blender session Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc', should_save_as=True)" When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc', should_save_as=True)"
Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/temp/export.ifc" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/temp/export.ifc"
Scenario: Export IFC - with basic contents and saving as IfcJSON where import is not supported Scenario: Export IFC - with basic contents and saving as IfcJSON where import is not supported
Given an empty Blender session Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifcjson', should_save_as=True)" When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifcjson', should_save_as=True)"
Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/basic.ifc" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/basic.ifc"
Scenario: Export IFC - with basic contents and round-tripping an IfcZip Scenario: Export IFC - with basic contents and round-tripping an IfcZip
Given an empty Blender session Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifczip', should_save_as=True)" When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifczip', should_save_as=True)"
Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/basic.ifc" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/basic.ifc"
When an empty Blender session is started When an empty Blender session is started
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifczip')" And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifczip')"
@@ -867,14 +867,14 @@ Scenario: Export IFC - with basic contents and saving as a relative path
Given an empty Blender session Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I press "wm.save_mainfile(filepath='{cwd}/test/files/temp/export.blend')" When I press "wm.save_mainfile(filepath='{cwd}/test/files/temp/export.blend')"
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True, save_as_invoked=True)" And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True, save_as_invoked=True)"
Then "scene.BIMProperties.ifc_file" is "export.ifc" Then "scene.BIMProperties.ifc_file" is "export.ifc"
Scenario: Export IFC - with deleted objects synchronised Scenario: Export IFC - with deleted objects synchronised
Given an empty IFC project Given an empty IFC project
When the object "IfcBuildingStorey/My Storey" is selected When the object "IfcBuildingStorey/My Storey" is selected
And I delete the selected objects And I delete the selected objects
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
And an empty Blender session is started And an empty Blender session is started
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcBuildingStorey/My Storey" does not exist Then the object "IfcBuildingStorey/My Storey" does not exist
@@ -882,7 +882,7 @@ Scenario: Export IFC - with deleted objects synchronised
Scenario: Export IFC - with moved object location synchronised Scenario: Export IFC - with moved object location synchronised
Given an empty IFC project Given an empty IFC project
When the object "IfcBuildingStorey/My Storey" is moved to "0,0,1" When the object "IfcBuildingStorey/My Storey" is moved to "0,0,1"
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
And an empty Blender session is started And an empty Blender session is started
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcBuildingStorey/My Storey" is at "0,0,1" Then the object "IfcBuildingStorey/My Storey" is at "0,0,1"
@@ -891,7 +891,7 @@ Scenario: Export IFC - with moved grid axis location synchronised
Given an empty IFC project Given an empty IFC project
And I press "mesh.add_grid" And I press "mesh.add_grid"
When the object "IfcGridAxis/01" is moved to "1,0,0" When the object "IfcGridAxis/01" is moved to "1,0,0"
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
And an empty Blender session is started And an empty Blender session is started
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcGridAxis/01" bottom left corner is at "1,-2,0" Then the object "IfcGridAxis/01" bottom left corner is at "1,-2,0"
@@ -905,7 +905,7 @@ Scenario: Export IFC - with changed object scale synchronised
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
When the object "IfcWall/Cube" is scaled to "2" When the object "IfcWall/Cube" is scaled to "2"
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
And an empty Blender session is started And an empty Blender session is started
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcWall/Cube" dimensions are "4,4,4" Then the object "IfcWall/Cube" dimensions are "4,4,4"
+3 -3
View File
@@ -380,15 +380,15 @@ Scenario: Edit pset length property
Scenario: Edit qset length property Scenario: Edit qset length property
Given an empty IFC project Given an empty IFC project
And I press "mesh.add_clever_stair" And I press "mesh.add_clever_stair"
And I press "bim.calculate_all_quantities" And I press "bim.perform_quantity_take_off"
And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Qto_StairFlightBaseQuantities').id()" And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Qto_StairFlightBaseQuantities').id()"
And the variable "si_conversion" is "ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())" And the variable "si_conversion" is "ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())"
And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')" And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')"
# Testing Q_LENGTH type of prop # Testing Q_LENGTH type of prop
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH" Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397" And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2156.485"
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257" And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.156"
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350" When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35" Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35"
+5 -16
View File
@@ -35,34 +35,23 @@ Scenario: Execute qto method - formwork areas
And I add a cube of size "1" at "1,0,0" And I add a cube of size "1" at "1,0,0"
And the object "Cube" is selected And the object "Cube" is selected
And additionally the object "Cube.001" is selected And additionally the object "Cube.001" is selected
When I set "scene.BIMQtoProperties.qto_methods" to "FORMWORK" When I press "bim.calculate_formwork_area"
And I press "bim.execute_qto_method"
Then "scene.BIMQtoProperties.qto_result" is "21.5" Then "scene.BIMQtoProperties.qto_result" is "21.5"
Scenario: Execute qto method - side formwork areas Scenario: Execute qto method - side formwork areas
Given an empty Blender session Given an empty Blender session
And I add a cube And I add a cube
And the object "Cube" is selected And the object "Cube" is selected
When I set "scene.BIMQtoProperties.qto_methods" to "SIDE_FORMWORK" When I press "bim.calculate_side_formwork_area"
And I press "bim.execute_qto_method"
Then "scene.BIMQtoProperties.qto_result" is "16.0" Then "scene.BIMQtoProperties.qto_result" is "16.0"
Scenario: Assign objects base qto
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')"
When I press "bim.assign_objects_base_qto"
Then "active_object.PsetProperties.qto_name" is "Qto_WallBaseQuantities"
Scenario: Calculate all quantities Scenario: Calculate all quantities
Given an empty IFC project Given an empty IFC project
And I add a cube And I add a cube
And the object "Cube" is selected And the object "Cube" is selected
And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')"
When I press "bim.calculate_all_quantities" When I press "bim.perform_quantity_take_off"
And the variable "qset_id" is "{ifc}.by_type('IfcElementQuantity')[0].id()" And the variable "qset_id" is "{ifc}.by_type('IfcElementQuantity')[0].id()"
And I press "bim.enable_pset_editing(pset_id={qset_id}, obj='IfcWall/Cube', obj_type='Object')" And I press "bim.enable_pset_editing(pset_id={qset_id}, obj='IfcWall/Cube', obj_type='Object')"
And I press "bim.disable_pset_editing(obj='IfcWall/Cube', obj_type='Object')" Then "active_object.PsetProperties.active_pset_name" is "Qto_WallBaseQuantities"
Then "active_object.PsetProperties.qto_name" is "Qto_WallBaseQuantities" And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2000.0"
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is "2000.0"
@@ -32,7 +32,7 @@ Scenario: Add Labor Resource
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
Then nothing happens Then nothing happens
@@ -41,7 +41,7 @@ Scenario: Enable Editing Resource
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.enable_editing_resource(resource={crew_resource})" When I press "bim.enable_editing_resource(resource={crew_resource})"
Then nothing happens Then nothing happens
@@ -49,7 +49,7 @@ Scenario: Edit Resource
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.enable_editing_resource(resource={crew_resource})" When I press "bim.enable_editing_resource(resource={crew_resource})"
And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo" And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo"
When I press "bim.edit_resource()" When I press "bim.edit_resource()"
@@ -59,7 +59,7 @@ Scenario: Remove Resource
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.enable_editing_resource(resource={crew_resource})" When I press "bim.enable_editing_resource(resource={crew_resource})"
And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo" And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo"
When I press "bim.edit_resource()" When I press "bim.edit_resource()"
@@ -70,9 +70,9 @@ Scenario: Remove Parent Resource
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_time(resource={labor_resource})" When I press "bim.enable_editing_resource_time(resource={labor_resource})"
And I set "scene.BIMResourceProperties.resource_time_attributes.get('Name').string_value" to "TimelyFoo" And I set "scene.BIMResourceProperties.resource_time_attributes.get('Name').string_value" to "TimelyFoo"
When I press "bim.edit_resource_time()" When I press "bim.edit_resource_time()"
@@ -83,9 +83,9 @@ Scenario: Enable Editing Resource time
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_time(resource={labor_resource})" When I press "bim.enable_editing_resource_time(resource={labor_resource})"
Then nothing happens Then nothing happens
@@ -93,9 +93,9 @@ Scenario: Disable editing resource time
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I press "bim.enable_editing_resource_time(resource={labor_resource})" And I press "bim.enable_editing_resource_time(resource={labor_resource})"
When I press "bim.disable_editing_resource()" When I press "bim.disable_editing_resource()"
Then nothing happens Then nothing happens
@@ -104,9 +104,9 @@ Scenario: Edit Resource time
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_time(resource={labor_resource})" When I press "bim.enable_editing_resource_time(resource={labor_resource})"
And I set "scene.BIMResourceProperties.resource_time_attributes.get('ScheduleUsage').float_value" to "305.25" And I set "scene.BIMResourceProperties.resource_time_attributes.get('ScheduleUsage').float_value" to "305.25"
When I press "bim.edit_resource_time()" When I press "bim.edit_resource_time()"
@@ -116,9 +116,9 @@ Scenario: Enable Editing Resource Costs
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.enable_editing_resource_costs(resource={labor_resource})"
Then nothing happens Then nothing happens
@@ -126,9 +126,9 @@ Scenario: Disable Editing Resource Costs
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.enable_editing_resource_costs(resource={labor_resource})"
When I press "bim.disable_editing_resource()" When I press "bim.disable_editing_resource()"
Then nothing happens Then nothing happens
@@ -138,9 +138,9 @@ Scenario: Add Resource Fixed Cost Value
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.enable_editing_resource_costs(resource={labor_resource})"
When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")"
Then nothing happens Then nothing happens
@@ -149,12 +149,12 @@ Scenario: Edit Resource Cost Value
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.enable_editing_resource_costs(resource={labor_resource})"
When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")"
And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()"
When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})"
Then nothing happens Then nothing happens
@@ -162,12 +162,12 @@ Scenario: Enable Editing Resource Cost Value Formula
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.enable_editing_resource_costs(resource={labor_resource})"
When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")"
And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()"
When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})"
Then nothing happens Then nothing happens
@@ -175,12 +175,12 @@ Scenario: Edit Resource Cost Value Formula
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.enable_editing_resource_costs(resource={labor_resource})"
When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")"
And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()"
When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})"
And I set "scene.BIMResourceProperties.cost_value_formula" to "220*0.2" And I set "scene.BIMResourceProperties.cost_value_formula" to "220*0.2"
When I press "bim.edit_resource_cost_value_formula(cost_value={cost_value})" When I press "bim.edit_resource_cost_value_formula(cost_value={cost_value})"
@@ -190,12 +190,12 @@ Scenario: Edit Resource Cost Value
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.enable_editing_resource_costs(resource={labor_resource})"
When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")"
And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()"
When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})"
And I set "scene.BIMResourceProperties.cost_value_attributes.get('AppliedValue').float_value" to "1.00" And I set "scene.BIMResourceProperties.cost_value_attributes.get('AppliedValue').float_value" to "1.00"
When I press "bim.edit_resource_cost_value(cost_value={cost_value})" When I press "bim.edit_resource_cost_value(cost_value={cost_value})"
@@ -205,11 +205,11 @@ Scenario: Enable Editing Resource Quantity
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})" When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})"
When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityArea")" When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityTime")"
When I press "bim.enable_editing_resource_quantity(resource={labor_resource})" When I press "bim.enable_editing_resource_quantity(resource={labor_resource})"
Then nothing happens Then nothing happens
@@ -217,27 +217,26 @@ Scenario: Edit Resource Quantity
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})" When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})"
When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityArea")" When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityTime")"
When I press "bim.enable_editing_resource_quantity(resource={labor_resource})" When I press "bim.enable_editing_resource_quantity(resource={labor_resource})"
And I set "scene.BIMResourceProperties.quantity_attributes.get('AreaValue').float_value" to "50.00" And I set "scene.BIMResourceProperties.quantity_attributes.get('TimeValue').float_value" to "50.00"
And the variable "quantity_area" is "IfcStore.get_file().by_type('IfcQuantityArea')[0].id()" And the variable "quantity_time" is "IfcStore.get_file().by_type('IfcQuantityTime')[0].id()"
When I press "bim.edit_resource_quantity(physical_quantity={quantity_area})" When I press "bim.edit_resource_quantity(physical_quantity={quantity_time})"
Then nothing happens Then nothing happens
Scenario: Remove Resource Quantity Scenario: Remove Resource Quantity
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})" When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})"
When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityArea")" When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityTime")"
And the variable "quantity_area" is "IfcStore.get_file().by_type('IfcQuantityArea')[0].id()"
When I press "bim.remove_resource_quantity(resource={labor_resource})" When I press "bim.remove_resource_quantity(resource={labor_resource})"
Then nothing happens Then nothing happens
@@ -245,9 +244,9 @@ Scenario: Add Productivity data
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I set "scene.BIMResourceProperties.active_resource_index" to "1" And I set "scene.BIMResourceProperties.active_resource_index" to "1"
And I press "bim.add_productivity_data" And I press "bim.add_productivity_data"
And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True"
@@ -284,9 +283,9 @@ Scenario: Calculate Resource Work
And I press "bim.edit_pset(obj='IfcWall/Cube', obj_type='Object')" And I press "bim.edit_pset(obj='IfcWall/Cube', obj_type='Object')"
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I set "scene.BIMResourceProperties.active_resource_index" to "1" And I set "scene.BIMResourceProperties.active_resource_index" to "1"
And I press "bim.add_productivity_data" And I press "bim.add_productivity_data"
And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True"
@@ -294,41 +293,41 @@ Scenario: Calculate Resource Work
And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea"
And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5" And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5"
When I press "bim.edit_productivity_data()" When I press "bim.edit_productivity_data()"
And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()" And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()"
When I press "bim.calculate_resource_work(resource={labor_resource})" When I press "bim.calculate_resource_work(resource={labor_resource})"
Then nothing happens Then nothing happens
Scenario: Assign Resource Scenario: Assign Resource
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I add a cube And I add a cube
And the object "Cube" is selected And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.assign_resource(resource={labor_resource}, related_object="")" And I press "bim.assign_resource(resource={labor_resource})"
Then nothing happens Then nothing happens
Scenario: UnAssign Resource Scenario: Unassign Resource
Given an empty IFC project Given an empty IFC project
When I press "bim.load_resources" When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")" When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I add a cube And I add a cube
And the object "Cube" is selected And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.assign_resource(resource={labor_resource}, related_object="")" And I press "bim.assign_resource(resource={labor_resource})"
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.unassign_resource(resource={labor_resource}, related_object="")" And I press "bim.unassign_resource(resource={labor_resource})"
Then nothing happens Then nothing happens
+1 -3
View File
@@ -64,7 +64,7 @@ Scenario: Assign a type class to a cube
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class" And I press "bim.assign_class"
Then the object "IfcWallType/Cube" is an "IfcWallType" Then the object "IfcWallType/Cube" is an "IfcWallType"
And the object "IfcWallType/Cube" is in the collection "Types" And the object "IfcWallType/Cube" is in the collection "IfcTypeProduct"
And the object "IfcWallType/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcWallType/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
Scenario: Assign a spatial class to a cube Scenario: Assign a spatial class to a cube
@@ -88,7 +88,6 @@ Scenario: Assign a spatial class to a cube already in a collection
And I press "bim.assign_class" And I press "bim.assign_class"
Then the object "IfcSpace/Cube" is an "IfcSpace" Then the object "IfcSpace/Cube" is an "IfcSpace"
And the object "IfcSpace/Cube" is in the collection "IfcSpace/Cube" And the object "IfcSpace/Cube" is in the collection "IfcSpace/Cube"
And the collection "IfcSpace/Cube" is in the collection "IfcBuildingStorey/My Storey"
And the object "IfcSpace/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcSpace/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
Scenario: Assign a class to a cube in a collection Scenario: Assign a class to a cube in a collection
@@ -118,4 +117,3 @@ Scenario: Copy a storey
Then the object "IfcBuildingStorey/My Storey" and "IfcBuildingStorey/My Storey.001" are different elements Then the object "IfcBuildingStorey/My Storey" and "IfcBuildingStorey/My Storey.001" are different elements
And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey"
And the object "IfcBuildingStorey/My Storey.001" is in the collection "IfcBuildingStorey/My Storey.001" And the object "IfcBuildingStorey/My Storey.001" is in the collection "IfcBuildingStorey/My Storey.001"
And the collection "IfcBuildingStorey/My Storey.001" is in the collection "IfcBuilding/My Building"
@@ -12,5 +12,5 @@ Scenario: Select all walls
And I set "scene.BIMSearchProperties.facet" to "entity" And I set "scene.BIMSearchProperties.facet" to "entity"
And I press "bim.add_filter(index=0, type='entity', module='search')" And I press "bim.add_filter(index=0, type='entity', module='search')"
And I set "scene.BIMSearchProperties.filter_groups[0].filters[0].value" to "IfcWall" And I set "scene.BIMSearchProperties.filter_groups[0].filters[0].value" to "IfcWall"
When I press "bim.search" When I press "bim.search(property_group='BIMSearchProperties')"
Then the object "IfcWall/Cube" is selected Then the object "IfcWall/Cube" is selected
+15 -12
View File
@@ -27,7 +27,8 @@ Scenario: Assign container
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container" And I press "bim.enable_editing_container"
And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()"
When I press "bim.assign_container(structure={site})" And I press "bim.set_default_container(container={site})"
When I press "bim.assign_container()"
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site" Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
Scenario: Copy to container Scenario: Copy to container
@@ -37,10 +38,10 @@ Scenario: Copy to container
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcSite/My Site" is selected
And additionally the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container" And I press "bim.enable_editing_container"
When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" When I press "bim.copy_to_container"
And I press "bim.copy_to_container"
Then the object "IfcWall/Cube.001" is in the collection "IfcSite/My Site" Then the object "IfcWall/Cube.001" is in the collection "IfcSite/My Site"
Scenario: Reference structure Scenario: Reference structure
@@ -50,10 +51,10 @@ Scenario: Reference structure
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcBuilding/My Building" is selected
And additionally the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container" And I press "bim.enable_editing_container"
When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" When I press "bim.reference_structure"
And I press "bim.reference_structure"
Then nothing happens Then nothing happens
Scenario: Dereference structure Scenario: Dereference structure
@@ -63,10 +64,10 @@ Scenario: Dereference structure
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class" And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected And the object "IfcBuilding/My Building" is selected
And additionally the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container" And I press "bim.enable_editing_container"
When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" When I press "bim.reference_structure"
And I press "bim.reference_structure"
And I press "bim.dereference_structure" And I press "bim.dereference_structure"
Then nothing happens Then nothing happens
@@ -80,7 +81,8 @@ Scenario: Select container
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container" And I press "bim.enable_editing_container"
And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()"
And I press "bim.assign_container(structure={site})" And I press "bim.set_default_container(container={site})"
And I press "bim.assign_container()"
When I press "bim.select_container" When I press "bim.select_container"
Then nothing happens Then nothing happens
@@ -94,7 +96,8 @@ Scenario: Select similar container
And the object "IfcWall/Cube" is selected And the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container" And I press "bim.enable_editing_container"
And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()"
And I press "bim.assign_container(structure={site})" And I press "bim.set_default_container(container={site})"
And I press "bim.assign_container()"
When I press "bim.select_similar_container" When I press "bim.select_similar_container"
Then nothing happens Then nothing happens
@@ -75,7 +75,6 @@ Scenario: Disable editing style
Scenario: Load styles Scenario: Load styles
Given an empty IFC project Given an empty IFC project
And I press "bim.add_style"
When I press "bim.load_styles(style_type='IfcSurfaceStyle')" When I press "bim.load_styles(style_type='IfcSurfaceStyle')"
Then nothing happens Then nothing happens
+34 -3
View File
@@ -684,17 +684,17 @@ def prop_is_roughly_value(prop, value):
value = replace_variables(value) value = replace_variables(value)
is_value = False is_value = False
try: try:
exec(f'assert round(bpy.context.{prop}, 5) == "{value}"') exec(f'assert round(bpy.context.{prop}, 3) == "{value}"')
is_value = True is_value = True
except: except:
try: try:
exec(f"assert round(bpy.context.{prop}, 5) == {value}") exec(f"assert round(bpy.context.{prop}, 3) == {value}")
is_value = True is_value = True
except: except:
pass pass
if not is_value: if not is_value:
print(f"bpy.context.{prop}") print(f"bpy.context.{prop}")
actual_value = round(eval(f"bpy.context.{prop}"), 5) actual_value = round(eval(f"bpy.context.{prop}"), 3)
assert False, f"Value is {actual_value}" assert False, f"Value is {actual_value}"
@@ -868,6 +868,37 @@ def the_object_name_is_contained_in_container_name(name, container_name):
assert container.Name == container_name, f'Object "{name}" is in {container}' assert container.Name == container_name, f'Object "{name}" is in {container}'
@then(parsers.parse('the object "{name}" is contained in object "{container_name}"'))
def the_object_name_is_contained_in_object_container_name(name: str, container_name: str) -> None:
ifc = an_ifc_file_exists()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
container = ifcopenshell.util.element.get_container(element)
if not container:
assert False, f'Object "{name}" is not in any container'
container_obj = tool.Ifc.get_object(container)
assert container_obj.name == container_name, f'Object "{name}" is in {container_obj}'
@then(parsers.parse('the object "{name}" is aggregated by object "{aggregate_name}"'))
def the_object_name_is_aggregated_by_object_aggregate_name(name: str, aggregate_name: str) -> None:
ifc = an_ifc_file_exists()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
assert False, f'Object "{name}" is not aggregated by any element'
aggregate_obj = tool.Ifc.get_object(aggregate)
assert aggregate_obj.name == aggregate_name, f'Object "{name}" is aggregated by {aggregate_obj}'
@then(parsers.parse('the object "{name}" has no aggregate'))
def the_object_name_has_no_aggregate(name: str) -> None:
ifc = an_ifc_file_exists()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
assert False, f'Object "{name}" is aggregated by element "{aggregate}"'
@then(parsers.parse('the file "{name}" should contain "{value}"')) @then(parsers.parse('the file "{name}" should contain "{value}"'))
def the_file_name_should_contain_value(name, value): def the_file_name_should_contain_value(name, value):
name = replace_variables(name) name = replace_variables(name)
@@ -36,6 +36,7 @@ class TestAssignObject:
def test_run(self, ifc, aggregate, collector): def test_run(self, ifc, aggregate, collector):
aggregate.can_aggregate("relating_obj", "related_obj").should_be_called().will_return(True) aggregate.can_aggregate("relating_obj", "related_obj").should_be_called().will_return(True)
ifc.get_entity("relating_obj").should_be_called().will_return("relating_object") ifc.get_entity("relating_obj").should_be_called().will_return("relating_object")
aggregate.has_physical_body_representation("relating_object").should_be_called().will_return(False)
ifc.get_entity("related_obj").should_be_called().will_return("related_object") ifc.get_entity("related_obj").should_be_called().will_return("related_object")
ifc.run( ifc.run(
"aggregate.assign_object", products=["related_object"], relating_object="relating_object" "aggregate.assign_object", products=["related_object"], relating_object="relating_object"
+26
View File
@@ -26,6 +26,7 @@ class TestEditObjectPlacement:
ifc.get_entity("obj").should_be_called().will_return("element") ifc.get_entity("obj").should_be_called().will_return("element")
geometry.clear_cache("element").should_be_called() geometry.clear_cache("element").should_be_called()
geometry.clear_scale("obj").should_be_called() geometry.clear_scale("obj").should_be_called()
geometry.get_blender_offset_type("obj").should_be_called()
surveyor.get_absolute_matrix("obj").should_be_called().will_return("matrix") surveyor.get_absolute_matrix("obj").should_be_called().will_return("matrix")
ifc.run("geometry.edit_object_placement", product="element", matrix="matrix").should_be_called() ifc.run("geometry.edit_object_placement", product="element", matrix="matrix").should_be_called()
geometry.record_object_position("obj").should_be_called() geometry.record_object_position("obj").should_be_called()
@@ -193,6 +194,7 @@ class TestSwitchRepresentation:
def test_switching_to_a_freshly_loaded_representation(self, ifc, geometry): def test_switching_to_a_freshly_loaded_representation(self, ifc, geometry):
geometry.is_edited("obj").should_be_called().will_return(False) geometry.is_edited("obj").should_be_called().will_return(False)
geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") geometry.get_object_data("obj").should_be_called().will_return("current_obj_data")
geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False)
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
geometry.get_representation_data("representation").should_be_called().will_return(None) geometry.get_representation_data("representation").should_be_called().will_return(None)
geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return( geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return(
@@ -220,6 +222,7 @@ class TestSwitchRepresentation:
def test_switching_to_a_reloaded_representation_and_deleting_the_existing_data(self, ifc, geometry): def test_switching_to_a_reloaded_representation_and_deleting_the_existing_data(self, ifc, geometry):
geometry.is_edited("obj").should_be_called().will_return(False) geometry.is_edited("obj").should_be_called().will_return(False)
geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") geometry.get_object_data("obj").should_be_called().will_return("current_obj_data")
geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False)
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
geometry.get_representation_data("representation").should_be_called().will_return("existing_data") geometry.get_representation_data("representation").should_be_called().will_return("existing_data")
geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return( geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return(
@@ -248,13 +251,35 @@ class TestSwitchRepresentation:
def test_switching_to_an_existing_representation(self, ifc, geometry): def test_switching_to_an_existing_representation(self, ifc, geometry):
geometry.is_edited("obj").should_be_called().will_return(False) geometry.is_edited("obj").should_be_called().will_return(False)
ifc.get_entity("obj").should_be_called().will_return("element")
geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") geometry.get_object_data("obj").should_be_called().will_return("current_obj_data")
geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False)
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
geometry.get_representation_data("representation").should_be_called().will_return("data") geometry.get_representation_data("representation").should_be_called().will_return("data")
geometry.change_object_data("obj", "data", is_global=True).should_be_called() geometry.change_object_data("obj", "data", is_global=True).should_be_called()
geometry.record_object_materials("obj").should_be_called() geometry.record_object_materials("obj").should_be_called()
geometry.clear_modifiers("obj").should_be_called() geometry.clear_modifiers("obj").should_be_called()
geometry.clear_cache("element").should_be_called()
subject.switch_representation(
ifc,
geometry,
obj="obj",
representation="mapped_rep",
should_reload=False,
is_global=True,
should_sync_changes_first=True,
)
def test_switching_to_an_existing_representation_reuse_representation(self, ifc, geometry):
geometry.is_edited("obj").should_be_called().will_return(False)
ifc.get_entity("obj").should_be_called().will_return("element") ifc.get_entity("obj").should_be_called().will_return("element")
geometry.get_object_data("obj").should_be_called().will_return("current_obj_data")
geometry.should_use_immediate_representation("element", True).should_be_called().will_return(True)
geometry.unresolve_type_representation("mapped_rep", "element").should_be_called().will_return("representation")
geometry.get_representation_data("representation").should_be_called().will_return("data")
geometry.change_object_data("obj", "data", is_global=False).should_be_called()
geometry.record_object_materials("obj").should_be_called()
geometry.clear_modifiers("obj").should_be_called()
geometry.clear_cache("element").should_be_called() geometry.clear_cache("element").should_be_called()
subject.switch_representation( subject.switch_representation(
ifc, ifc,
@@ -273,6 +298,7 @@ class TestSwitchRepresentation:
geometry.run_geometry_update_representation(obj="obj").should_be_called() geometry.run_geometry_update_representation(obj="obj").should_be_called()
geometry.does_representation_id_exist("representation_id").should_be_called().will_return(True) geometry.does_representation_id_exist("representation_id").should_be_called().will_return(True)
geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") geometry.get_object_data("obj").should_be_called().will_return("current_obj_data")
geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False)
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
geometry.get_representation_data("representation").should_be_called().will_return("data") geometry.get_representation_data("representation").should_be_called().will_return("data")
geometry.change_object_data("obj", "data", is_global=False).should_be_called() geometry.change_object_data("obj", "data", is_global=False).should_be_called()
+54 -9
View File
@@ -18,13 +18,13 @@
import blenderbim.core.project as subject import blenderbim.core.project as subject
from test.core.bootstrap import ifc, project from test.core.bootstrap import ifc, project, spatial, georeference
class TestCreateProject: class TestCreateProject:
def test_do_nothing_if_a_project_already_exists(self, ifc, project): def test_do_nothing_if_a_project_already_exists(self, ifc, georeference, project, spatial):
ifc.get().should_be_called().will_return("ifc") ifc.get().should_be_called().will_return("ifc")
subject.create_project(ifc, project, schema="IFC4", template=None) subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template=None)
def check_contexts(self, project): def check_contexts(self, project):
project.run_context_add_context( project.run_context_add_context(
@@ -67,7 +67,7 @@ class TestCreateProject:
context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent="plan" context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent="plan"
).should_be_called() ).should_be_called()
def test_create_an_ifc4_project(self, ifc, project): def test_create_an_ifc4_project(self, ifc, georeference, project, spatial):
ifc.get().should_be_called().will_return(None) ifc.get().should_be_called().will_return(None)
ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc") ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc")
ifc.set("ifc").should_be_called() ifc.set("ifc").should_be_called()
@@ -92,14 +92,17 @@ class TestCreateProject:
project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called() project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called()
project.set_context("body").should_be_called() project.set_context("body").should_be_called()
spatial.run_spatial_import_spatial_decomposition().should_be_called()
spatial.guess_default_container().should_be_called().will_return(None)
project.load_default_thumbnails().should_be_called() project.load_default_thumbnails().should_be_called()
project.set_default_context().should_be_called() project.set_default_context().should_be_called()
project.set_default_modeling_dimensions().should_be_called() project.set_default_modeling_dimensions().should_be_called()
georeference.set_model_origin().should_be_called()
subject.create_project(ifc, project, schema="IFC4", template=None) subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template=None)
def test_appending_project_template_types_if_specified(self, ifc, project): def test_create_an_ifc4_project_with_guessing_default_container(self, ifc, georeference, project, spatial):
ifc.get().should_be_called().will_return(None) ifc.get().should_be_called().will_return(None)
ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc") ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc")
ifc.set("ifc").should_be_called() ifc.set("ifc").should_be_called()
@@ -124,16 +127,55 @@ class TestCreateProject:
project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called() project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called()
project.set_context("body").should_be_called() project.set_context("body").should_be_called()
spatial.run_spatial_import_spatial_decomposition().should_be_called()
spatial.guess_default_container().should_be_called().will_return("default_container")
spatial.set_default_container("default_container").should_be_called()
project.load_default_thumbnails().should_be_called()
project.set_default_context().should_be_called()
project.set_default_modeling_dimensions().should_be_called()
georeference.set_model_origin().should_be_called()
subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template=None)
def test_appending_project_template_types_if_specified(self, ifc, georeference, project, spatial):
ifc.get().should_be_called().will_return(None)
ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc")
ifc.set("ifc").should_be_called()
project.create_empty("My Project").should_be_called().will_return("project")
project.create_empty("My Site").should_be_called().will_return("site")
project.create_empty("My Building").should_be_called().will_return("building")
project.create_empty("My Storey").should_be_called().will_return("storey")
project.run_root_assign_class(
obj="project", ifc_class="IfcProject", should_add_representation=False
).should_be_called()
project.run_unit_assign_scene_units().should_be_called()
self.check_contexts(project)
project.run_root_assign_class(obj="site", ifc_class="IfcSite", context="body").should_be_called()
project.run_root_assign_class(obj="building", ifc_class="IfcBuilding", context="body").should_be_called()
project.run_root_assign_class(obj="storey", ifc_class="IfcBuildingStorey", context="body").should_be_called()
project.run_aggregate_assign_object(relating_obj="project", related_obj="site").should_be_called()
project.run_aggregate_assign_object(relating_obj="site", related_obj="building").should_be_called()
project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called()
project.set_context("body").should_be_called()
spatial.run_spatial_import_spatial_decomposition().should_be_called()
spatial.guess_default_container().should_be_called().will_return(None)
project.append_all_types_from_template("template").should_be_called() project.append_all_types_from_template("template").should_be_called()
project.load_default_thumbnails().should_be_called() project.load_default_thumbnails().should_be_called()
project.set_default_context().should_be_called() project.set_default_context().should_be_called()
project.set_default_modeling_dimensions().should_be_called() project.set_default_modeling_dimensions().should_be_called()
georeference.set_model_origin().should_be_called()
subject.create_project(ifc, project, schema="IFC4", template="template") subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template="template")
def test_create_an_ifc2x3_project_with_owner_defaults(self, ifc, project): def test_create_an_ifc2x3_project_with_owner_defaults(self, ifc, georeference, project, spatial):
ifc.get().should_be_called().will_return(None) ifc.get().should_be_called().will_return(None)
ifc.run("project.create_file", version="IFC2X3").should_be_called().will_return("ifc") ifc.run("project.create_file", version="IFC2X3").should_be_called().will_return("ifc")
ifc.set("ifc").should_be_called() ifc.set("ifc").should_be_called()
@@ -165,9 +207,12 @@ class TestCreateProject:
project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called() project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called()
project.set_context("body").should_be_called() project.set_context("body").should_be_called()
spatial.run_spatial_import_spatial_decomposition().should_be_called()
spatial.guess_default_container().should_be_called().will_return(None)
project.load_default_thumbnails().should_be_called() project.load_default_thumbnails().should_be_called()
project.set_default_context().should_be_called() project.set_default_context().should_be_called()
project.set_default_modeling_dimensions().should_be_called() project.set_default_modeling_dimensions().should_be_called()
georeference.set_model_origin().should_be_called()
subject.create_project(ifc, project, schema="IFC2X3", template=None) subject.create_project(ifc, georeference, project, spatial, schema="IFC2X3", template=None)
+44 -4
View File
@@ -116,7 +116,9 @@ class TestAssignClass:
ifc_representation_class="ifc_representation_class", ifc_representation_class="ifc_representation_class",
) )
def test_assign_a_class_with_geometry_and_autodetected_spatial_container(self, ifc, collector, root): def test_assign_a_class_with_geometry_and_autodetected_spatial_container_spatial_element(
self, ifc, collector, root
):
ifc.get_entity("obj").should_be_called().will_return(None) ifc.get_entity("obj").should_be_called().will_return(None)
root.get_object_name("obj").should_be_called().will_return("name") root.get_object_name("obj").should_be_called().will_return("name")
ifc.run( ifc.run(
@@ -127,7 +129,11 @@ class TestAssignClass:
root.run_geometry_add_representation( root.run_geometry_add_representation(
obj="obj", context="context", ifc_representation_class="ifc_representation_class", profile_set_usage=None obj="obj", context="context", ifc_representation_class="ifc_representation_class", profile_set_usage=None
).should_be_called() ).should_be_called()
collector.sync("obj").should_be_called()
root.get_default_container().should_be_called().will_return("default_container")
root.is_spatial_element("element").should_be_called().will_return(True)
ifc.run("aggregate.assign_object", products=["element"], relating_object="default_container").should_be_called()
collector.assign("obj").should_be_called() collector.assign("obj").should_be_called()
subject.assign_class( subject.assign_class(
ifc, ifc,
@@ -141,7 +147,9 @@ class TestAssignClass:
ifc_representation_class="ifc_representation_class", ifc_representation_class="ifc_representation_class",
) )
def test_not_adding_a_representation_if_requested(self, ifc, collector, root): def test_assign_a_class_with_geometry_and_autodetected_spatial_container_non_spatial_containable(
self, ifc, collector, root
):
ifc.get_entity("obj").should_be_called().will_return(None) ifc.get_entity("obj").should_be_called().will_return(None)
root.get_object_name("obj").should_be_called().will_return("name") root.get_object_name("obj").should_be_called().will_return("name")
ifc.run( ifc.run(
@@ -149,7 +157,39 @@ class TestAssignClass:
).should_be_called().will_return("element") ).should_be_called().will_return("element")
root.set_object_name("obj", "element").should_be_called() root.set_object_name("obj", "element").should_be_called()
ifc.link("element", "obj").should_be_called() ifc.link("element", "obj").should_be_called()
collector.sync("obj").should_be_called() root.run_geometry_add_representation(
obj="obj", context="context", ifc_representation_class="ifc_representation_class", profile_set_usage=None
).should_be_called()
root.get_default_container().should_be_called().will_return("default_container")
root.is_spatial_element("element").should_be_called().will_return(False)
root.is_containable("element").should_be_called().will_return(True)
ifc.run(
"spatial.assign_container", products=["element"], relating_structure="default_container"
).should_be_called()
collector.assign("obj").should_be_called()
subject.assign_class(
ifc,
collector,
root,
obj="obj",
ifc_class="ifc_class",
predefined_type="predefined_type",
should_add_representation=True,
context="context",
ifc_representation_class="ifc_representation_class",
)
def test_not_adding_a_representation_if_requested_no_default_container(self, ifc, collector, root):
ifc.get_entity("obj").should_be_called().will_return(None)
root.get_object_name("obj").should_be_called().will_return("name")
ifc.run(
"root.create_entity", ifc_class="ifc_class", predefined_type="predefined_type", name="name"
).should_be_called().will_return("element")
root.set_object_name("obj", "element").should_be_called()
ifc.link("element", "obj").should_be_called()
root.get_default_container().should_be_called().will_return(None)
collector.assign("obj").should_be_called() collector.assign("obj").should_be_called()
subject.assign_class( subject.assign_class(
ifc, ifc,
+6 -17
View File
@@ -38,24 +38,21 @@ class TestDereferenceStructure:
class TestAssignContainer: class TestAssignContainer:
def test_run(self, ifc, collector, spatial): def test_run(self, ifc, collector, spatial):
spatial.can_contain("structure_obj", "element_obj").should_be_called().will_return(True) spatial.can_contain("container", "element_obj").should_be_called().will_return(True)
ifc.get_entity("structure_obj").should_be_called().will_return("structure")
ifc.get_entity("element_obj").should_be_called().will_return("element") ifc.get_entity("element_obj").should_be_called().will_return("element")
ifc.run( ifc.run(
"spatial.assign_container", products=["element"], relating_structure="structure" "spatial.assign_container", products=["element"], relating_structure="container"
).should_be_called().will_return("rel") ).should_be_called().will_return("rel")
spatial.disable_editing("element_obj").should_be_called() spatial.disable_editing("element_obj").should_be_called()
collector.assign("element_obj").should_be_called() collector.assign("element_obj").should_be_called()
assert ( assert (
subject.assign_container(ifc, collector, spatial, structure_obj="structure_obj", element_obj="element_obj") subject.assign_container(ifc, collector, spatial, container="container", element_obj="element_obj") == "rel"
== "rel"
) )
class TestEnableEditingContainer: class TestEnableEditingContainer:
def test_run(self, spatial): def test_run(self, spatial):
spatial.enable_editing("obj").should_be_called() spatial.enable_editing("obj").should_be_called()
spatial.import_containers().should_be_called()
subject.enable_editing_container(spatial, obj="obj") subject.enable_editing_container(spatial, obj="obj")
@@ -65,12 +62,6 @@ class TestDisableEditingContainer:
subject.disable_editing_container(spatial, obj="obj") subject.disable_editing_container(spatial, obj="obj")
class TestChangeSpatialLevel:
def test_run(self, spatial):
spatial.import_containers(parent="parent").should_be_called()
subject.change_spatial_level(spatial, parent="parent")
class TestRemoveContainer: class TestRemoveContainer:
def test_run(self, ifc, collector): def test_run(self, ifc, collector):
ifc.get_entity("obj").should_be_called().will_return("element") ifc.get_entity("obj").should_be_called().will_return("element")
@@ -90,7 +81,7 @@ class TestCopyToContainer:
spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj") spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj")
spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called() spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called()
spatial.run_root_copy_class(obj="new_obj").should_be_called() spatial.run_root_copy_class(obj="new_obj").should_be_called()
spatial.run_spatial_assign_container(structure_obj="to_container_obj", element_obj="new_obj").should_be_called() spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called()
spatial.disable_editing("obj").should_be_called() spatial.disable_editing("obj").should_be_called()
@@ -105,7 +96,7 @@ class TestCopyToContainer:
spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj") spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj")
spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called() spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called()
spatial.run_root_copy_class(obj="new_obj").should_be_called() spatial.run_root_copy_class(obj="new_obj").should_be_called()
spatial.run_spatial_assign_container(structure_obj="to_container_obj", element_obj="new_obj").should_be_called() spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called()
spatial.disable_editing("obj").should_be_called() spatial.disable_editing("obj").should_be_called()
@@ -114,11 +105,9 @@ class TestCopyToContainer:
class TestSelectContainer: class TestSelectContainer:
def test_run(self, ifc, spatial): def test_run(self, ifc, spatial):
ifc.get_entity("obj").should_be_called().will_return("element")
spatial.get_container("element").should_be_called().will_return("container")
ifc.get_object("container").should_be_called().will_return("container_obj") ifc.get_object("container").should_be_called().will_return("container_obj")
spatial.set_active_object("container_obj").should_be_called() spatial.set_active_object("container_obj").should_be_called()
subject.select_container(ifc, spatial, obj="obj") subject.select_container(ifc, spatial, container="container")
class TestSelectSimilarContainer: class TestSelectSimilarContainer:
+11 -6
View File
@@ -17,7 +17,7 @@
# 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 blenderbim.core.type as subject import blenderbim.core.type as subject
from test.core.bootstrap import ifc, type from test.core.bootstrap import ifc, type, geometry
class TestAssignType: class TestAssignType:
@@ -42,11 +42,16 @@ class TestAssignType:
class TestPurgeUnusedTypes: class TestPurgeUnusedTypes:
def test_run(self, ifc, type): def test_purge_types_obj_found(self, ifc, type, geometry):
type.get_model_types().should_be_called().will_return(["element_type"]) type.get_model_types().should_be_called().will_return(["element_type"])
type.get_type_occurrences("element_type").should_be_called().will_return([]) type.get_type_occurrences("element_type").should_be_called().will_return([])
ifc.run("root.remove_product", product="element_type").should_be_called()
ifc.get_object("element_type").should_be_called().will_return("obj") ifc.get_object("element_type").should_be_called().will_return("obj")
ifc.unlink(element="element_type").should_be_called() geometry.delete_ifc_object("obj").should_be_called()
type.remove_object("obj").should_be_called() subject.purge_unused_types(ifc, type, geometry)
subject.purge_unused_types(ifc, type)
def test_purge_types_obj_not_found(self, ifc, type, geometry):
type.get_model_types().should_be_called().will_return(["element_type"])
type.get_type_occurrences("element_type").should_be_called().will_return([])
ifc.get_object("element_type").should_be_called().will_return(None)
ifc.run("root.remove_product", product="element_type").should_be_called()
subject.purge_unused_types(ifc, type, geometry)
+6 -46
View File
@@ -40,7 +40,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcWall() element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None) element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj) tool.Ifc.link(element, element_obj)
assert subject.can_contain(structure_obj, element_obj) is True assert subject.can_contain(structure, element_obj) is True
def test_a_spatial_structure_element_can_contain_an_element_ifc2x3(self): def test_a_spatial_structure_element_can_contain_an_element_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3") ifc = ifcopenshell.file(schema="IFC2X3")
@@ -51,7 +51,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcWall() element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None) element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj) tool.Ifc.link(element, element_obj)
assert subject.can_contain(structure_obj, element_obj) is True assert subject.can_contain(structure, element_obj) is True
def test_a_spatial_zone_element_cannot_contain_an_element(self): def test_a_spatial_zone_element_cannot_contain_an_element(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
@@ -62,7 +62,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcWall() element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None) element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj) tool.Ifc.link(element, element_obj)
assert subject.can_contain(structure_obj, element_obj) is False assert subject.can_contain(structure, element_obj) is False
def test_unlinked_elements_cannot_contain_anything(self): def test_unlinked_elements_cannot_contain_anything(self):
structure_obj = bpy.data.objects.new("Object", None) structure_obj = bpy.data.objects.new("Object", None)
@@ -78,7 +78,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcWall() element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None) element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj) tool.Ifc.link(element, element_obj)
assert subject.can_contain(structure_obj, element_obj) is False assert subject.can_contain(structure, element_obj) is False
def test_a_non_element_cannot_be_contained(self): def test_a_non_element_cannot_be_contained(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
@@ -89,7 +89,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcTask() element = ifc.createIfcTask()
element_obj = bpy.data.objects.new("Object", None) element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj) tool.Ifc.link(element, element_obj)
assert subject.can_contain(structure_obj, element_obj) is False assert subject.can_contain(structure, element_obj) is False
def test_other_non_elements_that_have_a_contained_in_structure_attribute_can_be_contained(self): def test_other_non_elements_that_have_a_contained_in_structure_attribute_can_be_contained(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
@@ -100,7 +100,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcGrid() element = ifc.createIfcGrid()
element_obj = bpy.data.objects.new("Object", None) element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj) tool.Ifc.link(element, element_obj)
assert subject.can_contain(structure_obj, element_obj) is True assert subject.can_contain(structure, element_obj) is True
class TestCanReference(NewFile): class TestCanReference(NewFile):
@@ -181,46 +181,6 @@ class TestGetRelativeObjectMatrix(NewFile):
assert subject.get_relative_object_matrix(obj, relative_obj)[0][3] == -1 assert subject.get_relative_object_matrix(obj, relative_obj)[0][3] == -1
class TestImportContainers(NewFile):
def test_run(self):
bpy.ops.bim.create_project()
subject.import_containers()
props = bpy.context.scene.BIMSpatialProperties
assert len(props.containers) == 1
assert props.containers[0].name == "My Site"
assert props.containers[0].long_name == ""
assert props.containers[0].has_decomposition is True
assert props.containers[0].ifc_definition_id == tool.Ifc.get().by_type("IfcSite")[0].id()
assert props.active_container_id == tool.Ifc.get().by_type("IfcProject")[0].id()
def test_importing_with_a_specified_parent(self):
bpy.ops.bim.create_project()
site = tool.Ifc.get().by_type("IfcSite")[0]
subject.import_containers(site)
props = bpy.context.scene.BIMSpatialProperties
assert len(props.containers) == 1
assert props.containers[0].name == "My Building"
assert props.containers[0].long_name == ""
assert props.containers[0].has_decomposition is True
assert props.containers[0].ifc_definition_id == tool.Ifc.get().by_type("IfcBuilding")[0].id()
assert props.active_container_id == site.id()
def test_importing_sorted_by_z_placement(self):
bpy.ops.bim.create_project()
building = tool.Ifc.get().by_type("IfcBuilding")[0]
storey1 = tool.Ifc.get().by_type("IfcBuildingStorey")[0]
storey1.Name = "Higher"
bpy.ops.bim.copy_class(obj=tool.Ifc.get_object(storey1).name)
storey2 = tool.Ifc.get().by_type("IfcBuildingStorey")[1]
storey2.Name = "Lower"
storey2.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, -100.0)
subject.import_containers(building)
props = bpy.context.scene.BIMSpatialProperties
assert len(props.containers) == 2
assert props.containers[0].name == "Lower"
assert props.containers[1].name == "Higher"
class TestRunRootCopyClass(NewFile): class TestRunRootCopyClass(NewFile):
def test_nothing(self): def test_nothing(self):
pass pass
-7
View File
@@ -168,13 +168,6 @@ class TestHasMaterialUsage(NewFile):
assert subject.has_material_usage(element) is True assert subject.has_material_usage(element) is True
class TestRemoveObject(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
subject.remove_object(obj)
assert not bpy.data.objects.get("Object")
class TestRunGeometryAddRepresentation(NewFile): class TestRunGeometryAddRepresentation(NewFile):
def test_nothing(self): def test_nothing(self):
pass pass
+3 -1
View File
@@ -22,4 +22,6 @@ include ../common.mk
.PHONY: test .PHONY: test
test: test:
pytest -p no:pytest-blender test # Using python -m pytest vs just using pytest is important here.
# As just using pytest does not add current directory to PATH.
python -m pytest -p no:pytest-blender tests
+4 -2
View File
@@ -21,10 +21,12 @@ import json
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.api.pset import ifcopenshell.api.pset
import ifcopenshell.util.element
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.selector import ifcopenshell.util.selector
import multiprocessing import multiprocessing
from collections import namedtuple from collections import namedtuple
from typing import Any
Function = namedtuple("Function", ["measure", "name", "description"]) Function = namedtuple("Function", ["measure", "name", "description"])
@@ -47,7 +49,7 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst
return results return results
def edit_qtos(ifc_file, results) -> None: def edit_qtos(ifc_file: ifcopenshell.file, results: dict[ifcopenshell.entity_instance, Any]) -> None:
for element, qtos in results.items(): for element, qtos in results.items():
for name, quantities in qtos.items(): for name, quantities in qtos.items():
qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False) qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False)
@@ -59,7 +61,7 @@ def edit_qtos(ifc_file, results) -> None:
class SI2ProjectUnitConverter: class SI2ProjectUnitConverter:
def __init__(self, ifc_file): def __init__(self, ifc_file: ifcopenshell.file):
self.project_units = { self.project_units = {
"IfcAreaMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "AREAUNIT"), "IfcAreaMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "AREAUNIT"),
"IfcLengthMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT"), "IfcLengthMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT"),
@@ -391,6 +391,31 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
return true; return true;
} }
#include <BRepPrimAPI_MakeRevol.hxx>
bool IfcGeom::OpenCascadeKernel::convert_impl(const taxonomy::revolve::ptr r, IfcGeom::ConversionResults& results) {
gp_Ax1 ax(
convert_xyz<gp_Pnt>(*r->axis_origin),
convert_xyz<gp_Dir>(*r->direction));
TopoDS_Shape face;
if (!convert(taxonomy::cast<taxonomy::face>(r->basis), face)) {
return false;
}
TopoDS_Shape shape = BRepPrimAPI_MakeRevol(face, ax);
results.emplace_back(ConversionResult(
r->instance->data().id(),
r->matrix,
new OpenCascadeShape(shape),
r->surface_style
));
return true;
}
// IfcSchema::IfcRelVoidsElement::list::ptr IfcGeom::Kernel::find_openings(IfcSchema::IfcProduct* product) { // IfcSchema::IfcRelVoidsElement::list::ptr IfcGeom::Kernel::find_openings(IfcSchema::IfcProduct* product) {
// std::vector<IfcSchema::IfcRelVoidsElement*> rs; // std::vector<IfcSchema::IfcRelVoidsElement*> rs;
// //
@@ -128,7 +128,7 @@ public:
{} {}
bool convert(const ifcopenshell::geometry::taxonomy::extrusion::ptr, TopoDS_Shape&); bool convert(const ifcopenshell::geometry::taxonomy::extrusion::ptr, TopoDS_Shape&);
bool convert(const ifcopenshell::geometry::taxonomy::face::ptr, TopoDS_Shape&); bool convert(const ifcopenshell::geometry::taxonomy::face::ptr, TopoDS_Shape&, bool reversed_surface = false);
bool convert(const ifcopenshell::geometry::taxonomy::loop::ptr, TopoDS_Wire&); bool convert(const ifcopenshell::geometry::taxonomy::loop::ptr, TopoDS_Wire&);
bool convert(const ifcopenshell::geometry::taxonomy::matrix4::ptr, gp_GTrsf&); bool convert(const ifcopenshell::geometry::taxonomy::matrix4::ptr, gp_GTrsf&);
bool convert(const ifcopenshell::geometry::taxonomy::shell::ptr, TopoDS_Shape&); bool convert(const ifcopenshell::geometry::taxonomy::shell::ptr, TopoDS_Shape&);
@@ -143,6 +143,7 @@ public:
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::solid::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::solid::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::shell::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::shell::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::extrusion::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::extrusion::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::revolve::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::boolean_result::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::boolean_result::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::loft::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::loft::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::sweep_along_curve::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::sweep_along_curve::ptr, IfcGeom::ConversionResults&);
+19 -5
View File
@@ -229,7 +229,7 @@ Handle(Geom_Surface) OpenCascadeKernel::convert_surface(const taxonomy::ptr surf
} }
} }
bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& result) { bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& result, bool reversed_surface) {
#ifdef IFOPSH_DEBUG #ifdef IFOPSH_DEBUG
std::ostringstream oss; std::ostringstream oss;
face->print(oss); face->print(oss);
@@ -382,7 +382,11 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
} }
} }
} else if (!fd.all_outer()) { } else if (!fd.all_outer()) {
BRepBuilderAPI_MakeFace mf(fd.surface(), fd.outer_wire()); auto surf = fd.surface();
if (reversed_surface) {
surf = surf->UReversed();
}
BRepBuilderAPI_MakeFace mf(surf, fd.outer_wire());
if (mf.IsDone()) { if (mf.IsDone()) {
// Is this necessary // Is this necessary
@@ -434,10 +438,20 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
for (; jt.More(); jt.Next()) { for (; jt.More(); jt.Next()) {
Message_ListIteratorOfListOfMsg kt(jt.Value()); Message_ListIteratorOfListOfMsg kt(jt.Value());
for (; kt.More(); kt.Next()) { for (; kt.More(); kt.Next()) {
char* c = new char[kt.Value().Value().LengthOfCString() + 1]; char* c = new char[kt.Value().Original().LengthOfCString() + 1];
kt.Value().Value().ToUTF8CString(c); kt.Value().Original().ToUTF8CString(c);
Logger::Notice(c, face->instance); std::string message = c;
delete[] c; delete[] c;
#if OCC_VERSION_MAJOR==7 && OCC_VERSION_MINOR == 7
if (!reversed_surface && !fd.surface().IsNull() && fd.surface()->IsUPeriodic() && message == "Unknown message invoked with the keyword FixAdvFace.FixOrientation.MSG0") {
Logger::Notice("Detected reversed wire, reattempting with reversed basis surface");
TopoDS_Face reversed_result;
convert(face, reversed_result, true);
result = reversed_result;
return true;
} else
#endif
Logger::Warning(message, face->instance);
} }
} }
} }
@@ -21,6 +21,7 @@ possible at compile-time when using C++ and at run-time when using Python.
:caption: Contents: :caption: Contents:
ifcopenshell/installation ifcopenshell/installation
ifcopenshell/getting_started
ifcopenshell/geometry_iterator ifcopenshell/geometry_iterator
ifcopenshell/geometry_settings ifcopenshell/geometry_settings
ifcopenshell/boolean_process ifcopenshell/boolean_process
@@ -0,0 +1,115 @@
Getting Started with IFC parsing
================================
The basis of all parsing and getting information from the IFC starts with obtaining a ``IfcParse::IfcFile`` object and validating that it is good for use.
.. code-block:: c++
std::string input_file_path = "/path/to/my/model.ifc";
IfcParse::IfcFile file(input_file_path);
if (!file.good()) {
std::cerr << "Unable to parse .ifc file" << std::endl;
return 1;
}
Schema-agnostic parsing of IFCs
-------------------------------
It is advisable to design your programme in a schema-agnostic fashion to be able to process all schema versions of input IFCs.
While different options are presented `here <http://blog.ifcopenshell.org/2019/12/v060.html>`__, the most comfortable approach for modern C++ developers
would be with the use of templates as shown below.
.. code-block:: c++
// necessary includes
// For BOOST_PP_SEQ_FOR_EACH and BOOST_PP_STRINGIZE preprocessor macro
#include <boost/preprocessor/seq/for_each.hpp>
// Include all possible schema types that could be parsed
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/Ifc4.h"
#include "ifcparse/Ifc4x3_add2.h"
#define IFC_SCHEMA_SEQ (4x3_rc2)(4)(2x3) // TODO: Enumerate through all IFC schemas you want to be able to process
#define EXPAND_AND_CONCATENATE(elem) Ifc##elem
#define PROCESS_FOR_SCHEMA(r, data, elem) if (schema_version == BOOST_PP_STRINGIZE(elem)) { parseIfc<EXPAND_AND_CONCATENATE(elem)>(file); } else
template<typename Schema>
void parseIfc(IfcParse::IfcFile &file) {
const typename Schema::IfcProduct::list::ptr elements = file.instances_by_type<typename Schema::IfcProduct>();
for (typename Schema::IfcProduct::list::it it = elements->begin();
it != elements->end(); ++it) {
Schema::IfcProduct *ifcProduct = *it;
// TODO: Do something with ifcProduct
}
}
void process(const std::string &schema_version, IfcParse::IfcFile &file) {
// Syntactic sugar for iterating through all IFC schemas and passing them to main processing method
BOOST_PP_SEQ_FOR_EACH(PROCESS_FOR_SCHEMA, ,IFC_SCHEMA_SEQ)
{ // The final else to catch unhandled schema version
throw std::invalid_argument("IFC Schema " + schema_version + " not supported");
}
}
int main(int argc, char* argv[]) {
// file of IfcParse::IfcFile previously defined
auto schema_version = file.schema()->name();
schema_version = schema_version.substr(3);
std::transform(schema_version.begin(), schema_version.end(), schema_version.begin(), [](unsigned char c) { return std::tolower(c); });
process(schema_version, file);
// TODO: Handle more cases of IFC schema versions here
}
Reading out attributes of an IfcProduct
---------------------------------------
The properties of an ``IfcProduct``, and by extension any derived class, can be read as by calling the method of the same name, e.g. ``GlobalId()``, ``Name()``.
Note that optional properties like name, long name, or description, among others, are wrapped with a ``boost::optional`` (`documentation <https://www.boost.org/doc/libs/1_84_0/libs/optional/doc/html/index.html>`__) container object. Except for pointer types such as IfcRoot::OwnerHistory, here the developer is still responsible for performing comparison to `nullptr` before using the value.
Navigating relationships in IFC
-------------------------------
While it is natural to look at the IFC format as a representation of a physical building model where physical objects are related to each other in a tree structure (e.g. site > building > storey > slab),
the IFC format allows for the representation of information in a graph-like manner, joining physical objects to meta-information through relationships.
A detailed explanation of relationships in IFC is provided in this `blog post <https://constructingdata.wordpress.com/2018/04/09/ifc-for-the-layman-part-3-relationships/>`__.
The following function shows how property sets can be extracted from a given ``IfcObject``.
.. code-block:: c++
template<typename Schema>
void extractPropertySets(const typename Schema::IfcObject &obj,
typename std::vector<typename Schema::IfcPropertySet *> &property_sets) {
const auto &is_defined_by = obj.IsDefinedBy();
if (is_defined_by == nullptr)
return;
std::vector<typename Schema::IfcRelDefines *> rels(is_defined_by->size());
typename std::vector<typename Schema::IfcRelDefines *>::iterator it = std::copy_if(
is_defined_by->begin(), is_defined_by->end(), rels.begin(),
[](const typename Schema::IfcRelDefines *x) {
if (x == nullptr) return false;
const typename Schema::IfcRelDefinesByProperties *defines_by_properties =
x->template as<typename Schema::IfcRelDefinesByProperties>();
if (defines_by_properties == nullptr) return false;
const auto *relating_property_definition =
defines_by_properties->RelatingPropertyDefinition();
if (relating_property_definition == nullptr) return false;
return relating_property_definition->template as<typename Schema::IfcPropertySet>() !=
nullptr;
});
rels.resize(std::distance(rels.begin(), it));
property_sets.resize(rels.size());
std::transform(rels.begin(), rels.end(), property_sets.begin(),
[](const typename Schema::IfcRelDefines *x) {
const typename Schema::IfcRelDefinesByProperties *defines_by_properties = x->template as<typename Schema::IfcRelDefinesByProperties>();
return defines_by_properties->RelatingPropertyDefinition()->template as<typename Schema::IfcPropertySet>();
});
}
Defensive programming with IfcOpenshell
---------------------------------------
The need for (down-)casting object pointers when accessing various properties in an IFC entity is evident from the previous code sample as the methods and properties usually
return the abstract class of the entity. It is hence important to check for ``nullptr`` when performing such casts.
The existence of certain fields and properties should also be checked. Also note that IfcOpenShell has as of now not been tested explicitly against malicious inputs. Schema validation (the correctness of attribute types and conformance to schema where rules) can currently only be assessed in Python (using `ifcopenshell.validate --rules`).
@@ -17,14 +17,15 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell import ifcopenshell
from typing import Optional, Literal import ifcopenshell.util.representation
from typing import Optional
def add_context( def add_context(
file: ifcopenshell.file, file: ifcopenshell.file,
context_type: Optional[Literal["Model", "Plan"]] = None, context_type: Optional[ifcopenshell.util.representation.CONTEXT_TYPE] = None,
context_identifier: Optional[str] = None, context_identifier: Optional[ifcopenshell.util.representation.REPRESENTATION_IDENTIFIER] = None,
target_view: Optional[str] = None, target_view: Optional[ifcopenshell.util.representation.TARGET_VIEW] = None,
parent: Optional[ifcopenshell.entity_instance] = None, parent: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
"""Adds a new geometric representation context """Adds a new geometric representation context
@@ -106,7 +107,6 @@ def add_context(
the common target views above or consult the IFC documentation the common target views above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional (under the IfcShapeRepresentation page) for more details. Optional
for contexts, but mandatory for subcontexts. for contexts, but mandatory for subcontexts.
:type target_view: str, optional
:param parent: the parent context. Must be left as None (the default) :param parent: the parent context. Must be left as None (the default)
for contexts, and only set for subcontexts. Note that there are only for contexts, and only set for subcontexts. Note that there are only
contexts and subcontexts, a subcontext cannot have any children. contexts and subcontexts, a subcontext cannot have any children.
+4 -4
View File
@@ -382,10 +382,10 @@ class file:
for (attr_index, _), attr_name in zip(kwargs_attrs, kwargs): for (attr_index, _), attr_name in zip(kwargs_attrs, kwargs):
if attr_index == 0xFFFFFFFF: if attr_index == 0xFFFFFFFF:
invalid_attrs.append(attr_name) invalid_attrs.append(attr_name)
raise ValueError( raise ValueError(
"entity instance of type '%s' doesn't have the following attributes: %s." "entity instance of type '%s' doesn't have the following attributes: %s."
% (e.is_a(True), ", ".join(invalid_attrs)) % (e.is_a(True), ", ".join(invalid_attrs))
) )
# Restore transaction status # Restore transaction status
if attrs: if attrs:
@@ -41,6 +41,7 @@ except ImportError:
class shape_tuple(NamedTuple): class shape_tuple(NamedTuple):
"""A tuple containing IfcOpenShell serialized element/shape and pythonOCC shape.""" """A tuple containing IfcOpenShell serialized element/shape and pythonOCC shape."""
data: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization] data: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization]
geometry: TopoDS.TopoDS_Shape geometry: TopoDS.TopoDS_Shape
styles: tuple[tuple[float, float, float, float], ...] styles: tuple[tuple[float, float, float, float], ...]
+5
View File
@@ -26,6 +26,7 @@ import typing
import inspect import inspect
import collections import collections
import importlib import importlib
import importlib.util
from typing import Union from typing import Union
@@ -177,6 +178,10 @@ def _extract_docs(cls, method_name, boilerplate_args):
param_name = line.split(":")[1].strip().replace("param ", "") param_name = line.split(":")[1].strip().replace("param ", "")
if param_name in inputs: if param_name in inputs:
inputs[param_name]["description"] = line.split(":")[2].strip() inputs[param_name]["description"] = line.split(":")[2].strip()
elif line.startswith(":filter_glob"):
param_name = line.split(":")[1].strip().replace("filter_glob ", "")
if param_name in inputs:
inputs[param_name]["filter_glob"] = line.split(":")[2].strip()
elif i == 2: elif i == 2:
description += line description += line
elif i > 2: elif i > 2:
@@ -17,12 +17,15 @@
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>. # along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.pset import ifcopenshell.util.pset
import ifcopenshell.util.element import ifcopenshell.util.element
from logging import Logger
from typing import Union
class Patcher: class Patcher:
def __init__(self, src, file, logger, property_name=None, quantity_name=None): def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, property_name: str, quantity_name: str):
"""Converts a property to a standardised quantity """Converts a property to a standardised quantity
IFC can store arbitrary key value metadata associated with a elements IFC can store arbitrary key value metadata associated with a elements
@@ -40,14 +43,12 @@ class Patcher:
:param property_name: The name of the property to convert into a :param property_name: The name of the property to convert into a
quantity. The name of the property set is not considered. quantity. The name of the property set is not considered.
:type property_name: str
:param quantity_name: The name of the quantity that this property should :param quantity_name: The name of the quantity that this property should
be stored in. This should be a standard name that is one of the be stored in. This should be a standard name that is one of the
quantity names of a buildingSMART quantity template. For example, it quantity names of a buildingSMART quantity template. For example, it
may be "NetSideArea" for walls, which exists in may be "NetSideArea" for walls, which exists in
Qto_WallBaseQuantities. The quantity set name will be based on the Qto_WallBaseQuantities. The quantity set name will be based on the
standard buildingSMART quantity template. standard buildingSMART quantity template.
:type quantity_name: str
Example: Example:
@@ -63,7 +64,7 @@ class Patcher:
self.source_property_name = property_name self.source_property_name = property_name
self.destination_quantity_name = quantity_name self.destination_quantity_name = quantity_name
def patch(self): def patch(self) -> None:
self.qto_template_cache = {} self.qto_template_cache = {}
self.psetqto = ifcopenshell.util.pset.get_template("IFC4") self.psetqto = ifcopenshell.util.pset.get_template("IFC4")
@@ -76,7 +77,9 @@ class Patcher:
[r.RelatingPropertyDefinition for r in product.IsDefinedBy if r.is_a("IfcRelDefinesByProperties")], [r.RelatingPropertyDefinition for r in product.IsDefinedBy if r.is_a("IfcRelDefinesByProperties")],
) )
def process_product(self, product, definitions): def process_product(
self, product: ifcopenshell.entity_instance, definitions: list[ifcopenshell.entity_instance]
) -> None:
value = None value = None
has_quantity = False has_quantity = False
qtos = {} qtos = {}
@@ -98,12 +101,12 @@ class Patcher:
"pset.edit_qto", self.file, qto=qto, Properties={self.destination_quantity_name: value} "pset.edit_qto", self.file, qto=qto, Properties={self.destination_quantity_name: value}
) )
def get_qto_name(self, ifc_class): def get_qto_name(self, ifc_class: str) -> Union[str, None]:
for template in self.get_qto_templates(ifc_class): for template in self.get_qto_templates(ifc_class):
if self.destination_quantity_name in [t.Name for t in template.HasPropertyTemplates]: if self.destination_quantity_name in [t.Name for t in template.HasPropertyTemplates]:
return template.Name return template.Name
def get_qto_templates(self, ifc_class): def get_qto_templates(self, ifc_class: str) -> list[ifcopenshell.entity_instance]:
if ifc_class not in self.qto_template_cache: if ifc_class not in self.qto_template_cache:
self.qto_template_cache[ifc_class] = self.psetqto.get_applicable(ifc_class, qto_only=True) self.qto_template_cache[ifc_class] = self.psetqto.get_applicable(ifc_class, qto_only=True)
return self.qto_template_cache[ifc_class] return self.qto_template_cache[ifc_class]
+10 -4
View File
@@ -40,6 +40,8 @@ class Patcher:
:param filepath: The filepath of the second IFC model to merge into the :param filepath: The filepath of the second IFC model to merge into the
first. The first model is already specified as the input to first. The first model is already specified as the input to
IfcPatch. IfcPatch.
:type filepath: Union[str, ifcopenshell.file]
:filter_glob filepath: *.ifc;*.ifczip;*.ifcxml
Example: Example:
@@ -53,11 +55,15 @@ class Patcher:
self.filepath = filepath self.filepath = filepath
def patch(self): def patch(self):
if isinstance(self.filepath, ifcopenshell.file): for filepath in self.filepath:
other = self.filepath if isinstance(filepath, ifcopenshell.file):
else: other = filepath
other = ifcopenshell.open(self.filepath) else:
other = ifcopenshell.open(filepath)
self.merge(other)
def merge(self, other):
if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(other): if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(other):
other = ifcopenshell.util.unit.convert_file_length_units(other, main_unit) other = ifcopenshell.util.unit.convert_file_length_units(other, main_unit)
@@ -20,10 +20,23 @@ import math
import numpy as np import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.util.placement import ifcopenshell.util.placement
import typing
class Patcher: class Patcher:
def __init__(self, src, file, logger, x=None, y=None, z=None, should_rotate_first=True, ax=None, ay=None, az=None): def __init__(
self,
src,
file,
logger,
x: typing.Union[str, float] = "0",
y: typing.Union[str, float] = "0",
z: typing.Union[str, float] = "0",
should_rotate_first: bool = True,
ax: typing.Optional[typing.Union[str, float]] = "",
ay: typing.Optional[typing.Union[str, float]] = "",
az: typing.Optional[typing.Union[str, float]] = "",
):
"""Offset and rotate all object placements in a model """Offset and rotate all object placements in a model
Every physical object in an IFC model has an object placement, a Every physical object in an IFC model has an object placement, a
@@ -41,11 +54,11 @@ class Patcher:
entire IFC model. entire IFC model.
:param x: The X coordinate to offset by in project length units. :param x: The X coordinate to offset by in project length units.
:type x: float :type x: typing.Union[str, float]
:param y: The Y coordinate to offset by in project length units. :param y: The Y coordinate to offset by in project length units.
:type y: float :type y: typing.Union[str, float]
:param z: The Z coordinate to offset by in project length units. :param z: The Z coordinate to offset by in project length units.
:type z: float :type z: typing.Union[str, float]
:param should_rotate_first: Whether or not to rotate first and then :param should_rotate_first: Whether or not to rotate first and then
translate, or to first translate and rotate afterwards. Defaults to translate, or to first translate and rotate afterwards. Defaults to
rotate first then translate. rotate first then translate.
@@ -55,13 +68,13 @@ class Patcher:
around the Z axis). If all angle parameters are specified, then it around the Z axis). If all angle parameters are specified, then it
is treated as the angle to rotate around the X axis. Angles are in is treated as the angle to rotate around the X axis. Angles are in
decimal degrees and positive is anticlockwise. decimal degrees and positive is anticlockwise.
:type ax: float,optional :type ax: typing.Union[str, float],optional
:param ay: An optional angle to rotate by for 3D rotations along the Y :param ay: An optional angle to rotate by for 3D rotations along the Y
axis. Angles are in decimal degrees and positive is anticlockwise. axis. Angles are in decimal degrees and positive is anticlockwise.
:type ay: float,optional :type ay: typing.Union[str, float],optional
:param az: An optional angle to rotate by for 3D rotations along the Z :param az: An optional angle to rotate by for 3D rotations along the Z
axis. Angles are in decimal degrees and positive is anticlockwise. axis. Angles are in decimal degrees and positive is anticlockwise.
:type az: float,optional :type az: typing.Union[str, float],optional
Example: Example:
@@ -79,13 +92,21 @@ class Patcher:
self.src = src self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.x = x self.x = float(x)
self.y = y self.y = float(y)
self.z = z self.z = float(z)
self.should_rotate_first = should_rotate_first self.should_rotate_first = should_rotate_first
self.ax = ax self.ax = None
self.ay = ay self.ay = None
self.az = az self.az = None
try:
self.ax = float(ax)
self.ay = float(ay)
self.az = float(az)
except Exception:
print("At least one input angle is not specified.")
if self.ax is None: if self.ax is None:
self.angle_type = None self.angle_type = None
elif self.ay is None: elif self.ay is None:
@@ -16,15 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>. # along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
import typing
class Patcher: class Patcher:
def __init__(self, src, file, logger, z=None): def __init__(self, src, file, logger, z: typing.Union[str, float] = "0"):
"""Offset building storeys by a particular Z value """Offset building storeys by a particular Z value
All objects placed relative to the storeys will also be shifted. All objects placed relative to the storeys will also be shifted.
:param z: The Z value in project length units to offset storeys by. :param z: The Z value in project length units to offset storeys by.
:type z: float :type z: typing.Union[str, float]
Example: Example:
@@ -36,7 +38,7 @@ class Patcher:
self.src = src self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.z = z self.z = float(z)
def patch(self): def patch(self):
project = self.file.by_type("IfcProject")[0] project = self.file.by_type("IfcProject")[0]
+17 -16
View File
@@ -19,6 +19,7 @@
import ifcopenshell import ifcopenshell
import ifcopenshell.api.georeference import ifcopenshell.api.georeference
from ifcpatch.recipes import OffsetObjectPlacements, SetWorldCoordinateSystem from ifcpatch.recipes import OffsetObjectPlacements, SetWorldCoordinateSystem
import typing
class Patcher: class Patcher:
@@ -28,14 +29,14 @@ class Patcher:
file, file,
logger, logger,
name: str = "EPSG:1234", name: str = "EPSG:1234",
x: float = 0, x: typing.Union[str, float] = "0",
y: float = 0, y: typing.Union[str, float] = "0",
z: float = 0, z: typing.Union[str, float] = "0",
e: float = 0, e: typing.Union[str, float] = "0",
n: float = 0, n: typing.Union[str, float] = "0",
h: float = 0, h: typing.Union[str, float] = "0",
gn_angle: float = 0, gn_angle: typing.Union[str, float] = "0",
rotate_angle: float = 0, rotate_angle: typing.Union[str, float] = "0",
): ):
"""Sets a false origin with a map conversion in a model """Sets a false origin with a map conversion in a model
@@ -62,14 +63,14 @@ class Patcher:
self.file = file self.file = file
self.logger = logger self.logger = logger
self.name = name self.name = name
self.x = x self.x = float(x)
self.y = y self.y = float(y)
self.z = z self.z = float(z)
self.e = e self.e = float(e)
self.n = n self.n = float(n)
self.h = h self.h = float(h)
self.gn_angle = gn_angle self.gn_angle = float(gn_angle)
self.rotate_angle = rotate_angle self.rotate_angle = float(rotate_angle)
def patch(self): def patch(self):
SetWorldCoordinateSystem.Patcher(self.src, self.file, self.logger, x=0, y=0, z=0, ax=0, ay=0, az=0).patch() SetWorldCoordinateSystem.Patcher(self.src, self.file, self.logger, x=0, y=0, z=0, ax=0, ay=0, az=0).patch()

Some files were not shown because too many files have changed in this diff Show More