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: |
python -m pip install --upgrade pip
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 https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
@@ -125,4 +126,9 @@ jobs:
python tests.py
cd ../src/ifcopenshell-python
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]
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$'
extend-exclude = '(src/ifcopenshell-python/ifcopenshell/express/*|src/ifc2ca/templates/*)'
include = '''
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]
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.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd
.PHONY: test
test:
pytest -p no:pytest-blender tests
# .PHONY
# api:
# 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
# Provides IFCJSON functionality
cd build && wget https://github.com/IFCJSON-Team/IFC2JSON_python/archive/master.zip
cd build && unzip master.zip && rm master.zip
# TODO: replace with main repo if https://github.com/IFCJSON-Team/IFC2JSON_python/pull/3 is merged.
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.
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; \
setup( \
name='ifcjson', \
version='0.0.0', \
version='0.0.1', \
author='Jan Brouwer', \
author_email='jan@brewsky.nl', \
packages=['ifcjson'], \
)" 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.
# Provides Brickschema functionality
+1 -1
View File
@@ -121,6 +121,7 @@ classes = [
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
prop.MultipleFileSelect,
prop.Attribute,
prop.BIMAreaProperties,
prop.BIMTabProperties,
@@ -133,7 +134,6 @@ classes = [
prop.BIMMeshProperties,
prop.BIMFacet,
prop.BIMFilterGroup,
prop.MultipleFileSelect,
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
@@ -2,7 +2,9 @@ import sys
import os
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:
sys.path.insert(0, blenderbim_lib_path)
@@ -52,9 +54,16 @@ class WebNamespace(socketio.AsyncNamespace):
"web_operator",
data,
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):
# Send cached messages to the connected web client
for blenderId, messages in blender_messages.items():
@@ -98,6 +107,12 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["gantt_data"] = data
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
sio.register_namespace(WebNamespace("/web"))
@@ -108,26 +123,45 @@ sio.register_namespace(BlenderNamespace("/blender"))
async def index(request):
with open("templates/index.html", "r") as f:
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")
async def gantt(request):
with open("templates/gantt.html", "r") as f:
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})
return web.Response(text=html_content, content_type="text/html")
async def open_web_browser(app):
webbrowser.open(f"http://127.0.0.1:{sio_port}/")
async def on_startup(app):
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("/drawings", drawings)
app.router.add_get("/gantt", gantt)
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
app.router.add_static("/static/", path="./static", name="static")
app.on_startup.append(open_web_browser)
app.on_startup.append(on_startup)
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 {
--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-small: 0.125rem;
--padding-tiny: 0.125rem;
--padding-small: 0.625rem;
--padding-medium: 1rem;
--font-size-small: 1rem;
--font-size-large: 1.2rem;
--logo-height: 2.5rem;
--nav-height: 2.5rem;
--folder-collapse-font-size: 0.75rem;
--folder-collapse-font-family: Courier, "Courier New", monospace;
--box-shadow: 0 0 0.7rem #5f5f5f66;
@@ -31,6 +35,7 @@
--group-item-bg-color: #4b4b4b;
--input-bg-color: #3b3b3b;
--input-border-color: #000;
--details-border-color: #464444;
}
:root.light {
@@ -42,6 +47,7 @@
--nav-link-color: #000000;
--nav-link-hover-color: #38a63d;
--warning-color: #FF4500;
--details-border-color: #222;
}
html {
@@ -53,9 +59,13 @@ body {
color: var(--primary-text-color);
margin: 0;
font-family: var(--font-family);
display: flex;
flex-direction: column;
min-height: 100vh;
}
#container {
flex: 1;
margin-top: var(--margin-medium);
margin-left: var(--margin-small);
margin-right: var(--margin-small);
@@ -107,7 +117,7 @@ nav ul li a.active {
.warning {
color: var(--warning-color);
margin-bottom: var(--margin-small);
padding: var(--padding-small);
padding: var(--padding-tiny);
display: none;
}
@@ -141,6 +151,87 @@ nav ul li a.active {
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 {
margin: 0.5rem;
font-size: var(--font-size-small);
@@ -1,12 +1,16 @@
: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-small: 0.125rem;
--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 {
@@ -18,6 +22,7 @@
--nav-link-color: #fff;
--nav-link-hover-color: #3fb449;
--warning-color: #FFDB8F;
--table-border-color: #464444;
}
:root.light {
@@ -29,6 +34,7 @@
--nav-link-color: #000000;
--nav-link-hover-color: #38a63d;
--warning-color: #FF4500;
--table-border-color: #222;
}
html {
@@ -40,29 +46,26 @@ body {
color: var(--text-color);
margin: 0;
font-family: var(--font-family);
display: flex;
flex-direction: column;
min-height: 100vh;
}
#container {
flex: 1;
margin-top: var(--margin-medium);
margin-left: var(--margin-small);
margin-right: var(--margin-small);
margin-bottom: var(--margin-medium);
}
.table-container {
margin-bottom: var(--margin-large);
}
.csv-table {
margin-top: var(--margin-small);
}
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;
@@ -102,7 +105,7 @@ nav ul li a.active {
.warning {
color: var(--warning-color);
margin-bottom: var(--margin-small);
padding: var(--padding-small);
padding: var(--padding-tiny);
display: none;
}
@@ -118,6 +121,95 @@ nav ul li a.active {
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 ------------ */
:root.light .tabulator-header,
@@ -127,6 +219,6 @@ nav ul li a.active {
}
:root.light .tabulator {
border-top: 1px solid #222;
border-bottom: 2px solid #222;
border-top: 1px solid var(--table-border-color);
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];
removeGanttElement(blenderId);
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) - 1;
});
}
// Function to handle 'gantt_data' event
@@ -77,6 +81,7 @@ function handleGanttData(data) {
if (!connectedClients[blenderId].shown) {
connectedClients[blenderId] = {
shown: true,
ifc_file: filename,
ganttTasks: ganttTasks,
workSchedule: ganttWorkSched,
};
@@ -89,6 +94,7 @@ function handleGanttData(data) {
} else {
connectedClients[blenderId] = {
shown: true,
ifc_file: filename,
ganttTasks: ganttTasks,
workSchedule: ganttWorkSched,
};
@@ -105,6 +111,10 @@ function handleDefaultData(data) {
// Function to add a new gantt with data and filename
function addGanttElement(blenderId, tasks, workSched, filename) {
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
const ganttContainer = $("<div></div>")
.addClass("gantt-container")
.attr("id", "container-" + blenderId);
@@ -382,3 +392,75 @@ function toggleTheme() {
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];
removeTableElement(blenderId);
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) - 1;
});
}
// Function to handle 'csv_data' event
@@ -81,6 +85,10 @@ function handleDefaultData(data) {
// Function to add a new table with data and filename
function addTableElement(blenderId, csvData, filename) {
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
// store headers of the csv data
const firstLine = csvData.indexOf("\n");
const csvHeaders = csvData.substring(0, firstLine).split(",");
@@ -274,3 +282,66 @@ function toggleTheme() {
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>
<li><a href="/">IFC Data</a></li>
<li><a href="/gantt" class="active">Gantt Chart</a></li>
<li><a href="/drawings">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 id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
</footer>
</body>
</html>
@@ -41,11 +41,23 @@
<ul>
<li><a href="/" class="active">IFC Data</a></li>
<li><a href="/gantt">Gantt Chart</a></li>
<li><a href="/drawings">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 id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
</footer>
</body>
</html>
+5
View File
@@ -170,6 +170,11 @@ def subscribe_to(obj, data_path, callback):
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
for name, value in modules.items():
+2
View File
@@ -50,6 +50,8 @@ def draw_attribute(attribute, layout, copy_operator=None):
return
if value_name == "enum_value":
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"]:
propis = bpy.context.scene.BIMWorkScheduleProperties
for item in propis.durations_attributes:
@@ -167,7 +167,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc,
tool.Collector,
tool.Spatial,
structure_obj=tool.Ifc.get_object(current_container),
container=current_container,
element_obj=aggregate,
)
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.api
import ifcopenshell.guid
import ifcopenshell.util.element
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
@@ -36,7 +37,7 @@ class EnableEditingAttributes(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj)
obj = bpy.data.objects[self.obj]
props = obj.BIMAttributeProperties
props.attributes.clear()
@@ -1472,12 +1472,9 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
# save current visibility statuses for Views and Types collections
# save current visibility statuses
visibility_status: dict[bpy.types.Object, bool] = {}
for col in bpy.data.collections["Views"].children:
for obj in col.objects:
visibility_status[obj] = obj.hide_get()
for obj in bpy.data.collections["Types"].objects:
for obj in bpy.data.objects:
visibility_status[obj] = obj.hide_get()
if not bpy.app.background:
@@ -1508,6 +1505,7 @@ class ActivateModel(bpy.types.Operator):
obj.hide_set(hide_status)
tool.Blender.update_viewport()
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -116,7 +116,7 @@ class GeoreferenceDecorator:
arc_mid = angle_half @ arc_start
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"
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']}"
@@ -281,7 +281,7 @@ class GeoreferenceDecorator:
verts, edges = arc_segments
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:
verts = [Vector((0, 0, 0)), wcs["blender_location"]]
edges = [[0, 1]]
@@ -143,7 +143,6 @@ classes = (
stair.EnableEditingStair,
stair.RemoveStair,
pie.OpenPieClass,
pie.PieUpdateContainer,
pie.PieAddOpening,
pie.VIEW3D_MT_PIE_bim,
pie.VIEW3D_MT_PIE_bim_class,
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.core.covering as core
@@ -31,9 +32,7 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato
@classmethod
def poll(cls, context):
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))
return relating_type == "FLOORING"
return tool.Covering.covering_poll_relating_type_check(cls, context, "FLOORING")
def _execute(self, context):
try:
@@ -50,9 +49,7 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator
@classmethod
def poll(cls, context):
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))
return relating_type == "CEILING"
return tool.Covering.covering_poll_relating_type_check(cls, context, "CEILING")
def _execute(self, context):
try:
@@ -69,8 +66,10 @@ class RegenSelectedCoveringObject(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
element = tool.Ifc.get_entity(bpy.context.active_object)
return element and element.is_a("IfcCovering")
if (obj := context.active_object) and (element := tool.Ifc.get_entity(obj)) and element.is_a("IfcCovering"):
return True
cls.poll_message_set("IfcCovering must be selected.")
return False
def _execute(self, context):
try:
@@ -89,11 +88,7 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
@classmethod
def poll(cls, context):
element = tool.Ifc.get_entity(bpy.context.active_object)
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"
return tool.Covering.covering_poll_wall_selected(cls, context, "FLOORING")
def _execute(self, context):
# 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
def poll(cls, context):
element = tool.Ifc.get_entity(bpy.context.active_object)
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"
return tool.Covering.covering_poll_wall_selected(cls, context, "CEILING")
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -63,28 +63,6 @@ class PieAddOpening(bpy.types.Operator):
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):
bl_label = "Geometry"
@@ -93,7 +71,6 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu):
pie.operator("bim.edit_object_placement")
pie.operator("bim.update_representation").ifc_representation_class = ""
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.aggregate_assign_object", text="Assign Aggregation")
pie.operator("bim.aggregate_unassign_object", text="Unassign Aggregation")
@@ -225,9 +225,8 @@ class AddConstrTypeInstance(bpy.types.Operator):
else:
parent = ifcopenshell.util.element.get_container(building_element)
if parent:
parent_obj = tool.Ifc.get_object(parent)
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
@@ -122,9 +122,14 @@ class UpdateIfcPatchArguments(bpy.types.Operator):
new_attr = patch_args.add()
data_type = arg_info.get("type", "str")
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]
new_attr.data_type = {
"Literal": "enum",
"file": "file",
"str": "string",
"float": "float",
"int": "integer",
@@ -136,6 +141,11 @@ class UpdateIfcPatchArguments(bpy.types.Operator):
new_attr.enum_value = arg_info.get("default", new_attr.get_value_default())
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()))
return {"FINISHED"}
@@ -28,16 +28,15 @@ classes = (
operator.BIM_OT_load_clipping_planes,
operator.BIM_OT_save_clipping_planes,
operator.ChangeLibraryElement,
operator.ClearRecentIFCProjects,
operator.CreateClippingPlane,
operator.CreateProject,
operator.ClearRecentIFCProjects,
operator.DisableCulling,
operator.DisableEditingHeader,
operator.EditHeader,
operator.EnableCulling,
operator.EnableEditingHeader,
operator.ExportIFC,
operator.ExportIFCDeprecated,
operator.FlipClippingPlane,
operator.LinkIfc,
operator.LoadLink,
@@ -92,6 +91,7 @@ def register():
bpy.utils.register_tool(workspace.ExploreTool, after={"builtin.transform"}, separator=True, group=False)
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
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_context_menu.prepend(ui.file_menu)
wm = bpy.context.window_manager
@@ -730,12 +730,12 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
if not self.is_existing_ifc_file():
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:
bpy.data.objects.remove(obj)
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.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
tool.Blender.register_toolbar()
@@ -743,6 +743,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
if not self.is_advanced:
bpy.ops.bim.load_project_elements()
if not self.should_start_fresh_session:
bpy.ops.bim.convert_to_blender()
except:
blenderbim.last_error = traceback.format_exc()
raise
@@ -1235,6 +1237,10 @@ class ExportIFCBase:
if bpy.data.is_saved:
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):
if not tool.Ifc.get():
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
@@ -1333,18 +1339,6 @@ class ExportIFC(ExportIFCBase, bpy.types.Operator):
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):
bl_idname = "bim.load_linked_project"
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
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):
bl_idname = "BIM_MT_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")
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.load_project").should_start_fresh_session = False
class BIM_PT_project_library(Panel):
@@ -25,6 +25,7 @@ classes = (
operator.CalculateFaceAreas,
operator.CalculateFormworkArea,
operator.CalculateObjectVolumes,
operator.CalculateSideFormworkArea,
operator.CalculateSingleQuantity,
operator.PerformQuantityTakeOff,
prop.BIMQtoProperties,
@@ -18,25 +18,28 @@
import bpy
import bmesh
from typing import Callable
def calculate_height(obj):
def calculate_height(obj: bpy.types.Object) -> float:
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)))
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)))
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())
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
:param objs: iterable of mesh object
@@ -58,7 +61,7 @@ def calculate_mesh_quantity(objs: bpy.types.Object, context, operation):
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
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.BIMObjectProperties.ifc_definition_id = 0
modifier = copied_obj.modifiers.new("Formwork", "REMESH")
assert isinstance(modifier, bpy.types.RemeshModifier)
modifier.mode = "SHARP"
# 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.
@@ -108,7 +112,7 @@ def calculate_formwork_area(objs, context):
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
surfaces of one or more objects, excluding top and bottom surfaces (i.e.
@@ -99,6 +99,21 @@ class CalculateFormworkArea(bpy.types.Operator):
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):
bl_idname = "bim.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_label = "Perform Quantity Take-off"
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
def poll(cls, context):
@@ -162,4 +180,6 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
self.report({"INFO"}, f"Quantities are calculated for {len(elements)} elements.")
return {"FINISHED"}
@@ -175,6 +175,9 @@ class Search(Operator):
props = context.scene.CsvProperties
elif self.property_group == "BIMSearchProperties":
props = context.scene.BIMSearchProperties
else:
raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.")
results = ifcopenshell.util.selector.filter_elements(
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):
bl_idname = "bim.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"}
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()
for obj in context.selected_objects:
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
@@ -44,11 +54,21 @@ class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.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"}
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()
for obj in context.selected_objects:
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
@@ -59,6 +79,7 @@ class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_container"
bl_label = "Assign Container"
bl_description = "Assign current default container to the selected objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
@@ -99,29 +120,36 @@ class RemoveContainer(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
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
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_label = "Copy To Container"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
# Track decompositions so they can be recreated after the operation
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
old_to_new = {}
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()
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)
if result_objs:
old_to_new[tool.Ifc.get_entity(obj)] = result_objs
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
blenderbim.bim.handler.refresh_ui_data()
@@ -32,6 +32,7 @@ from bpy.props import (
)
import blenderbim.tool as tool
import ifcopenshell
import ifcopenshell.util.element
def get_subelement_class(self, context):
@@ -71,6 +72,7 @@ def update_relating_container_from_object(self, context):
return
container = ifcopenshell.util.element.get_container(element)
if container:
# TODO: currently broken and relating_container_object is not used in UI.
bpy.ops.bim.assign_container(structure=container.id())
else:
bpy.ops.bim.disable_editing_container()
@@ -36,6 +36,10 @@ class BIM_PT_spatial(Panel):
return SpatialData.data["poll"]
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:
SpatialData.load()
@@ -77,9 +81,15 @@ class BIM_PT_spatial(Panel):
else:
row.label(text="No Spatial Container")
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.label(text=reference, icon="LINKED")
else:
self.layout.label(text="No References In Structures")
class BIM_PT_spatial_decomposition(Panel):
@@ -567,5 +567,5 @@ class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
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.")
+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."""
bl_idname = "bim.multiple_file_selector"
bl_label = "Select Multiple Files"
bl_label = "Select File(s)"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
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)
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):
value_name = self.get_value_name()
if value_name:
@@ -248,6 +276,8 @@ class Attribute(PropertyGroup):
enum_items: StringProperty(name="Value")
enum_descriptions: CollectionProperty(type=StrProperty)
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_optional: BoolProperty(name="Is Optional")
is_uri: BoolProperty(name="Is Uri", default=False)
@@ -263,6 +293,8 @@ class Attribute(PropertyGroup):
return None
if self.data_type == "string":
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)
def get_value_default(self):
@@ -276,6 +308,8 @@ class Attribute(PropertyGroup):
return False
elif self.data_type == "enum":
return "0"
elif self.data_type == "file":
return ""
def get_value_name(self, display_only=False):
if self.data_type == "string":
@@ -290,6 +324,8 @@ class Attribute(PropertyGroup):
return "float_value"
elif self.data_type == "enum":
return "enum_value"
elif self.data_type == "file":
return "filepath_value"
def set_value(self, value):
if isinstance(value, str):
@@ -478,31 +514,3 @@ class BIMFacet(PropertyGroup):
class BIMFilterGroup(PropertyGroup):
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
# 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():
raise NoDefaultContainer()
@@ -32,7 +40,7 @@ def add_instance_flooring_covering_from_cursor(ifc, root, spatial):
relating_type = None
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:
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)
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():
raise NoDefaultContainer()
@@ -74,7 +84,7 @@ def add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial):
relating_type = None
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:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
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)
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():
raise NoDefaultContainer()
@@ -109,7 +119,7 @@ def regen_selected_covering_object(root, spatial):
selected_objects = spatial.get_selected_objects()
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)
@@ -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
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():
raise NoDefaultContainer()
@@ -158,7 +168,7 @@ def add_instance_flooring_coverings_from_walls(root, spatial):
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():
raise NoDefaultContainer()
+7 -2
View File
@@ -18,11 +18,12 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Union
if TYPE_CHECKING:
import bpy
import ifcopenshell
import ifcopenshell.util.representation
import blenderbim.tool as tool
@@ -225,7 +226,11 @@ def disable_editing_drawings(drawing: tool.Drawing) -> None:
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:
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)
+6 -4
View File
@@ -126,8 +126,7 @@ def switch_representation(
if not current_obj_data and geometry.is_text_literal(representation):
return
use_immediate_repr = apply_openings and bool(getattr(entity, "HasOpenings", None))
use_immediate_repr = use_immediate_repr or geometry.has_material_style_override(entity)
use_immediate_repr = geometry.should_use_immediate_representation(entity, apply_openings)
if use_immediate_repr:
# if it has openings make sure to switch to element's mapped representation
representation = geometry.unresolve_type_representation(representation, entity)
@@ -169,8 +168,11 @@ def get_representation_ifc_parameters(
def remove_representation(
ifc: tool.Ifc, geometry: tool.Geometry, obj: bpy.types.Object, representation: ifcopenshell.entity_instance
) -> None:
"""Consider changing obj representation before using the function,
otherwise it will replace object with empty."""
"""Remove IFC representation from an object.
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)
assert element
+2 -2
View File
@@ -95,7 +95,7 @@ def copy_to_container(
copied_obj = spatial.duplicate_object_and_data(obj)
spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix)
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)
return result_objs
@@ -173,7 +173,7 @@ def generate_space(ifc: tool.Ifc, model: tool.Model, root: tool.Root, spatial: t
relating_type = None
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)
else:
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
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 enable_editing(cls, obj): pass
def get_container(cls, element): pass
def get_relating_object(cls, related_element): pass
@interface
@@ -380,6 +382,7 @@ class Geometry:
def clear_modifiers(cls, obj): pass
def clear_scale(cls, obj): pass
def delete_data(cls, data): pass
def delete_ifc_object(cls, obj): pass
def does_representation_id_exist(cls, representation_id): pass
def duplicate_object_data(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 delete_opening_object_placement(cls, opening): 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
@@ -447,6 +453,7 @@ class Georeference:
def set_cursor_location(cls, coordinates): pass
def set_ifc_grid_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_wcs(cls, matrix): pass
def xyz2enh(cls, coordinates): pass
@@ -845,11 +852,10 @@ class Sequence:
@interface
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 contract_container(cls, container): 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 disable_editing(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 enable_editing(cls, obj): 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 get_active_container(cls): 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_selected_product_types(cls): pass
def get_selected_products(cls): pass
def import_containers(cls, parent=None): pass
def import_spatial_decomposition(cls): pass
def run_root_copy_class(cls, obj=None): pass
def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass
def import_spatial_element(cls, element, level_index): 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 select_object(cls, obj): pass
def select_products(cls, products, unhide=False): pass
def set_active_object(cls, obj): 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
#HERE STARTS SPATIAL TOOL
# HERE STARTS SPATIAL TOOL
def is_bounding_class(cls, visible_element): 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 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_union_shape_from_selected_objects(cls): 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_height(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_scaled_2d_vertices(cls, points): pass
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass
def get_body_representation(cls, obj): pass
def assign_ifcspace_class_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 regen_obj_representation(cls, ifc, geometry, obj, body): pass
def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass
def regen_obj_representation(cls, obj, body): pass
def toggle_spaces_visibility_wired_and_textured(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
class Covering:
@@ -1007,7 +1022,6 @@ class Type:
def get_representation_context(cls, representation): pass
def get_type_occurrences(cls, element_type): 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_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)
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."""
purged_types = 0
for element_type in type.get_model_types():
if not type.get_type_occurrences(element_type):
obj = ifc.get_object(element_type)
ifc.run("root.remove_product", product=element_type)
purged_types += 1
if obj:
ifc.unlink(element=element_type)
type.remove_object(obj)
geometry.delete_ifc_object(obj)
else:
ifc.run("root.remove_product", product=element_type)
purged_types += 1
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
if web.is_port_available(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)
+40 -16
View File
@@ -20,6 +20,7 @@ import bpy
import bmesh
import shapely
import ifcopenshell
import ifcopenshell.util.element
import blenderbim.core.tool
import blenderbim.core.root
import blenderbim.core.spatial
@@ -33,23 +34,46 @@ from shapely import Polygon, MultiPolygon
class Covering(blenderbim.core.tool.Covering):
@classmethod
def get_z_from_ceiling_height(cls):
def get_z_from_ceiling_height(cls) -> float:
props = bpy.context.scene.BIMCoveringProperties
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):
# 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
@classmethod
def covering_poll_wall_selected(
cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str
) -> bool:
if not context.selected_objects or not context.active_object:
operator.poll_message_set("No objects selected.")
return False
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":
operator.poll_message_set("LAYER2 based IfcWall must be selected.")
return False
return cls.covering_poll_relating_type_check(operator, context, covering_type)
@classmethod
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 re
import collections
import collections.abc
import bpy
import math
import json
@@ -79,11 +80,11 @@ class Drawing(blenderbim.core.tool.Drawing):
# fmt: on
@classmethod
def canonicalise_class_name(cls, name):
def canonicalise_class_name(cls, name: str) -> str:
return re.sub("[^0-9a-zA-Z]+", "", name)
@classmethod
def copy_representation(cls, source, dest):
def copy_representation(cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance) -> None:
if source.Representation:
dest.Representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"]
@@ -114,7 +115,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return obj
@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"""
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")
@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"""
if not related_object:
@@ -205,7 +210,9 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Drawing.update_text_value(obj)
@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):
object_types = [object_types]
@@ -226,7 +233,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return False
@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(
element, "Plan", "Annotation"
) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
@@ -237,7 +246,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return rep
@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.location = (0, 0, 1.5) # The view shall be 1.5m above the origin
camera.data.show_limits = True
@@ -257,7 +268,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return camera
@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
schedule_creator = scheduler.Scheduler()
@@ -276,7 +287,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return uri
@classmethod
def add_drawings(cls, sheet):
def add_drawings(cls, sheet: ifcopenshell.entity_instance) -> None:
import blenderbim.bim.module.drawing.sheeter as sheeter
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)
@classmethod
def delete_collection(cls, collection):
def delete_collection(cls, collection: bpy.types.Collection) -> None:
bpy.data.collections.remove(collection, do_unlink=True)
@classmethod
@@ -312,19 +323,19 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.data.objects.remove(obj)
@classmethod
def disable_editing_drawings(cls):
def disable_editing_drawings(cls) -> None:
bpy.context.scene.DocProperties.is_editing_drawings = False
@classmethod
def disable_editing_schedules(cls):
def disable_editing_schedules(cls) -> None:
bpy.context.scene.DocProperties.is_editing_schedules = False
@classmethod
def disable_editing_references(cls):
def disable_editing_references(cls) -> None:
bpy.context.scene.DocProperties.is_editing_references = False
@classmethod
def disable_editing_sheets(cls):
def disable_editing_sheets(cls) -> None:
bpy.context.scene.DocProperties.is_editing_sheets = False
@classmethod
@@ -344,19 +355,19 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.ops.object.mode_set(mode="EDIT")
@classmethod
def enable_editing_drawings(cls):
def enable_editing_drawings(cls) -> None:
bpy.context.scene.DocProperties.is_editing_drawings = True
@classmethod
def enable_editing_schedules(cls):
def enable_editing_schedules(cls) -> None:
bpy.context.scene.DocProperties.is_editing_schedules = True
@classmethod
def enable_editing_references(cls):
def enable_editing_references(cls) -> None:
bpy.context.scene.DocProperties.is_editing_references = True
@classmethod
def enable_editing_sheets(cls):
def enable_editing_sheets(cls) -> None:
bpy.context.scene.DocProperties.is_editing_sheets = True
@classmethod
@@ -368,14 +379,14 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.BIMAssignedProductProperties.is_editing_product = True
@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"]
while name in names:
name += "-X"
return name
@classmethod
def ensure_unique_identification(cls, identification):
def ensure_unique_identification(cls, identification: str) -> str:
if tool.Ifc.get_schema() == "IFC2X3":
ids = [d.DocumentId for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]
else:
@@ -431,7 +442,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", target_view)
@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")
@classmethod
@@ -462,15 +473,15 @@ class Drawing(blenderbim.core.tool.Drawing):
return os.path.splitext(os.path.basename(path))[0]
@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}"
@classmethod
def get_unit_system(cls):
def get_unit_system(cls) -> Literal["NONE", "METRIC", "IMPERIAL"]:
return bpy.context.scene.unit_settings.system
@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)
if obj:
return obj.BIMObjectProperties.collection
@@ -488,8 +499,8 @@ class Drawing(blenderbim.core.tool.Drawing):
return rel.RelatingDocument
@classmethod
def get_drawing_references(cls, drawing):
results = set()
def get_drawing_references(cls, drawing: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
results: set[ifcopenshell.entity_instance] = set()
for inverse in tool.Ifc.get().get_inverse(drawing):
if inverse.is_a("IfcRelAssignsToProduct") and inverse.RelatingProduct == drawing:
results.update(inverse.RelatedObjects)
@@ -505,7 +516,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return rel.RelatedObjects
@classmethod
def get_ifc_representation_class(cls, object_type):
def get_ifc_representation_class(cls, object_type: str) -> str:
if object_type == "TEXT":
return "IfcTextLiteral"
elif object_type == "TEXT_LEADER":
@@ -517,7 +528,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return element.Name
@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
if target_view == "PLAN_VIEW":
if location_hint:
@@ -554,12 +567,14 @@ class Drawing(blenderbim.core.tool.Drawing):
return mathutils.Matrix()
@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"])
return "A" + str(number).zfill(2)
@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)
if not element:
return
@@ -575,11 +590,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return items[0]
@classmethod
def is_editing_sheets(cls):
def is_editing_sheets(cls) -> bool:
return bpy.context.scene.DocProperties.is_editing_sheets
@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)
if not element:
return
@@ -617,7 +632,9 @@ class Drawing(blenderbim.core.tool.Drawing):
cls.remove_literal_from_annotation(obj, literal)
@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)
if not element:
return
@@ -831,7 +848,7 @@ class Drawing(blenderbim.core.tool.Drawing):
new.identification = schedule.Identification
@classmethod
def import_sheets(cls):
def import_sheets(cls) -> None:
props = bpy.context.scene.DocProperties
expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded}
props.sheets.clear()
@@ -868,7 +885,7 @@ class Drawing(blenderbim.core.tool.Drawing):
new.reference_type = reference_description
@classmethod
def get_active_sheet(cls, context):
def get_active_sheet(cls, context: bpy.types.Context) -> bpy.types.PropertyGroup:
props = context.scene.DocProperties
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
@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:
commands = json.loads(user_command)
replacements = {"path": path}
@@ -920,15 +937,15 @@ class Drawing(blenderbim.core.tool.Drawing):
subprocess.call(("xdg-open", path))
@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)
@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)
@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)
@classmethod
@@ -954,15 +971,17 @@ class Drawing(blenderbim.core.tool.Drawing):
)
@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)
@classmethod
def set_name(cls, element, name):
def set_name(cls, element: ifcopenshell.entity_instance, name: str) -> None:
element.Name = name
@classmethod
def show_decorations(cls):
def show_decorations(cls) -> None:
bpy.context.scene.DocProperties.should_draw_decorations = True
@classmethod
@@ -1013,15 +1032,15 @@ class Drawing(blenderbim.core.tool.Drawing):
# TODO below this point is highly experimental prototype code with no tests
@classmethod
def does_file_exist(cls, uri):
def does_file_exist(cls, uri: str) -> bool:
return os.path.exists(uri)
@classmethod
def delete_file(cls, uri):
def delete_file(cls, uri: str) -> None:
os.remove(uri)
@classmethod
def move_file(cls, src, dest):
def move_file(cls, src: str, dest: str) -> None:
try:
shutil.move(src, dest)
except:
@@ -1029,7 +1048,9 @@ class Drawing(blenderbim.core.tool.Drawing):
shutil.copy(src, dest)
@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:
location = tool.Ifc.get().by_id(location_hint)
if target_view == "REFLECTED_PLAN_VIEW":
@@ -1042,7 +1063,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return target_view
@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]
layouts_dir = (
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("\\", "/")
@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]
sheets_dir = (
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("\\", "/")
@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]
titleblocks_dir = (
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("\\", "/")
@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]
drawings_dir = (
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("\\", "/")
@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 "._- "))
@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]
resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr(
bpy.context.scene.DocProperties, f"{resource.lower()}_path"
@@ -1091,12 +1112,12 @@ class Drawing(blenderbim.core.tool.Drawing):
return resource_path.replace("\\", "/")
@classmethod
def get_default_shading_style(cls):
def get_default_shading_style(cls) -> str:
dprops = bpy.context.scene.DocProperties
return dprops.shadingstyle_default
@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)
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
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")
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.data = dst.data.copy()
dst.name = dst.name.replace("IfcGridAxis/", "")
@@ -1395,13 +1416,13 @@ class Drawing(blenderbim.core.tool.Drawing):
dst.data.BIMMeshProperties.ifc_definition_id = 0
return dst
def disassemble(obj):
def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
mesh = bmesh.new()
mesh.verts.ensure_lookup_table()
mesh.from_mesh(obj.data)
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)
return obj
@@ -1413,7 +1434,7 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.matrix_world.translation += annotation_offset
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()
points = [v.co for v in mesh.verts[0:2]]
points = helper.clip_segment(bounds, points)
@@ -1423,7 +1444,7 @@ class Drawing(blenderbim.core.tool.Drawing):
mesh.verts[1].co = points[1]
return mesh
def draw_grids_vertically(mesh):
def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]]
points = helper.elevate_segment(bounds, points)
@@ -1534,11 +1555,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return text
@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)
@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)
element = tool.Ifc.get_entity(obj)
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
@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_obj = tool.Ifc.get_object(grid)
if grid_obj:
@@ -1568,11 +1589,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return document.HasDocumentReferences or []
@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]
@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)
root = tree.getroot()
rel_location = os.path.relpath(new_location, os.path.dirname(uri))
@@ -1612,11 +1633,13 @@ class Drawing(blenderbim.core.tool.Drawing):
return attributes
@classmethod
def get_reference_location(cls, reference):
def get_reference_location(cls, reference: ifcopenshell.entity_instance) -> Union[str, None]:
return reference.Location
@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":
refs = [r for r in tool.Ifc.get().by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference]
else:
@@ -1625,12 +1648,12 @@ class Drawing(blenderbim.core.tool.Drawing):
return refs[0].RelatedObjects[0]
@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 {}
return "NTS" if pset.get("IsNTS", False) else pset.get("HumanScale", "NTS")
@classmethod
def get_drawing_metadata(cls, drawing):
def get_drawing_metadata(cls, drawing: ifcopenshell.entity_instance) -> list[str]:
# fmt: off
return [
v.strip()
@@ -1642,7 +1665,7 @@ class Drawing(blenderbim.core.tool.Drawing):
# fmt: on
@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
@classmethod
@@ -1654,11 +1677,11 @@ class Drawing(blenderbim.core.tool.Drawing):
return symbol
@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)
@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)
@classmethod
@@ -1723,19 +1746,21 @@ class Drawing(blenderbim.core.tool.Drawing):
return elements
@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:
if rel.is_a("IfcRelAssignsToProduct"):
return rel.RelatingProduct
@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:
if rel.is_a("IfcRelAssociatesDocument"):
return rel.RelatingDocument
@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":
return reference.ReferenceToDocument[0]
return reference.ReferencedDocument
@@ -1749,13 +1774,13 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Ifc.get_object(product).select_set(True)
@classmethod
def is_drawing_active(cls):
def is_drawing_active(cls) -> bool:
camera = bpy.context.scene.camera
area = tool.Blender.get_view3d_area()
return camera and camera.type == "CAMERA" and camera.BIMObjectProperties.ifc_definition_id and area
@classmethod
def is_camera_orthographic(cls):
def is_camera_orthographic(cls) -> bool:
camera = bpy.context.scene.camera
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
@classmethod
def run_drawing_activate_model(cls):
def run_drawing_activate_model(cls) -> None:
bpy.ops.bim.activate_model()
@classmethod
@@ -1906,7 +1931,15 @@ class Drawing(blenderbim.core.tool.Drawing):
)
@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_x = [v.x 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
@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
plane_co = camera.matrix_world.translation
plane_no = camera.matrix_world.col[2].xyz
return cls.is_intersecting_plane(obj, plane_co, plane_no)
@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
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]
@@ -1958,7 +1991,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return pos_exists and neg_exists
@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
plane_co = camera_matrix.translation
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)
@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:
global_offset = Vector()
@@ -1980,9 +2015,9 @@ class Drawing(blenderbim.core.tool.Drawing):
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)
vert_map = {}
verts = []
edges = []
vert_map: dict[int, int] = {}
verts: list[Vector] = []
edges: list[list[int]] = []
i = 0
for geom in results["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
@@ -1998,12 +2033,12 @@ class Drawing(blenderbim.core.tool.Drawing):
return verts, edges
@classmethod
def get_scale_ratio(cls, scale):
def get_scale_ratio(cls, scale: str) -> float:
numerator, denominator = scale.split("/")
return float(numerator) / float(denominator)
@classmethod
def get_diagram_scale(cls, obj):
def get_diagram_scale(cls, obj: bpy.types.Object) -> dict[str, float]:
props = obj.data.BIMCameraProperties
scale = props.diagram_scale
if scale != "CUSTOM":
@@ -2029,7 +2064,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return {"HumanScale": human_scale, "Scale": scale}
@classmethod
def convert_scale_string(cls, value):
def convert_scale_string(cls, value: str) -> float:
try:
return float(value)
except:
@@ -2060,7 +2095,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return result * 0.0254
@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)
end = np.array(end)
direction = end - start
@@ -2068,8 +2103,8 @@ class Drawing(blenderbim.core.tool.Drawing):
return (start - offset).tolist(), (end + offset).tolist()
@classmethod
def get_sheet_references(cls, drawing):
sheet_references = []
def get_sheet_references(cls, drawing: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
sheet_references: list[ifcopenshell.entity_instance] = []
drawing_reference = cls.get_drawing_document(drawing)
for sheet in tool.Ifc.get().by_type("IfcDocumentInformation"):
if not sheet.Scope == "SHEET":
@@ -2082,7 +2117,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return sheet_references
@classmethod
def get_camera_matrix(cls, camera):
def get_camera_matrix(cls, camera: bpy.types.Object) -> Matrix:
matrix_world = camera.matrix_world.copy().normalized()
location, rotation, scale = matrix_world.decompose()
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
for r in cls.get_representations_iter(element):
r = tool.Geometry.resolve_mapped_representation(r)
if r != representation:
new_representation = r
break
@@ -1227,3 +1228,9 @@ class Geometry(blenderbim.core.tool.Geometry):
return
material_style = tool.Material.get_style(materials[0])
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 ifcopenshell
import ifcopenshell.api.georeference
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import blenderbim.core.tool
import blenderbim.tool as tool
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 re
import bpy
@@ -5,6 +24,7 @@ import logging
from blenderbim.bim import import_ifc
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
from typing import TYPE_CHECKING, Union
# allows git import even if git executable isn't found
os.environ["GIT_PYTHON_REFRESH"] = "quiet"
@@ -13,15 +33,20 @@ try:
except ImportError:
print("Warning: GitPython not available.")
if TYPE_CHECKING:
import git
class IfcGit:
STEP_IDS = dict[str, set[int]]
@classmethod
def init_repo(cls, path_dir):
def init_repo(cls, path_dir: str) -> None:
IfcGitRepo.repo = git.Repo.init(path_dir)
cls.config_info_attributes(IfcGitRepo.repo)
@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(
url=remote_url,
to_path=local_folder,
@@ -30,7 +55,7 @@ class IfcGit:
return IfcGitRepo.repo
@classmethod
def load_anyifc(cls, repo):
def load_anyifc(cls, repo: git.Repo) -> bool:
working_dir = repo.working_dir
for item in os.listdir(working_dir):
path = os.path.join(working_dir, item)
@@ -40,11 +65,11 @@ class IfcGit:
return False
@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))
@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"""
if os.path.isdir(path):
@@ -72,7 +97,7 @@ class IfcGit:
return repo
@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":
cls.dos2unix(path_file)
repo.index.add(path_file)
@@ -80,11 +105,11 @@ class IfcGit:
bpy.ops.ifcgit.refresh()
@classmethod
def git_checkout(cls, path_file):
def git_checkout(cls, path_file: str) -> None:
IfcGitRepo.repo.git.checkout(path_file)
@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"""
props = bpy.context.scene.IfcGitProperties
if props.new_branch_name:
@@ -94,7 +119,7 @@ class IfcGit:
bpy.ops.ifcgit.refresh()
@classmethod
def git_commit(cls, path_file):
def git_commit(cls, path_file: str) -> None:
props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo
if os.name == "nt":
@@ -104,7 +129,7 @@ class IfcGit:
props.commit_message = ""
@classmethod
def add_tag(cls, repo):
def add_tag(cls, repo: git.Repo) -> None:
props = bpy.context.scene.IfcGitProperties
item = props.ifcgit_commits[props.commit_index]
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 = ""
@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:
repo.delete_tag(tag_name)
@classmethod
def add_remote(cls, repo):
def add_remote(cls, repo: git.Repo) -> None:
props = bpy.context.scene.IfcGitProperties
repo.create_remote(name=props.remote_name, url=props.remote_url)
props.remote_name = ""
props.remote_url = ""
@classmethod
def delete_remote(cls, repo):
def delete_remote(cls, repo: git.Repo) -> None:
props = bpy.context.scene.IfcGitProperties
remote_name = props.select_remote
if remote_name in repo.remotes:
@@ -133,7 +158,7 @@ class IfcGit:
props.select_remote = repo.remotes[0].name
@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)
remote = repo.remotes[remote_name]
try:
@@ -142,7 +167,7 @@ class IfcGit:
return exc.stderr
@classmethod
def create_new_branch(cls):
def create_new_branch(cls) -> None:
"""Convert a detached HEAD into a branch"""
props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo
@@ -154,7 +179,7 @@ class IfcGit:
bpy.ops.ifcgit.refresh()
@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.spaces[0].shading.color_type = "MATERIAL"
props = bpy.context.scene.IfcGitProperties
@@ -163,7 +188,7 @@ class IfcGit:
props.ifcgit_commits.clear()
@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
repo = cls.repo_from_path(path_ifc)
@@ -205,14 +230,14 @@ class IfcGit:
list_item.tags[-1].message = tag.tag.message
@classmethod
def refresh_revision_list(cls, path_ifc):
def refresh_revision_list(cls, path_ifc: str) -> None:
repo = cls.repo_from_path(path_ifc)
cls.clear_commits_list()
lookup = cls.tags_by_hexsha(repo)
cls.get_commits_list(path_ifc, lookup)
@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"""
return re.match(
@@ -221,7 +246,7 @@ class IfcGit:
)
@classmethod
def load_project(cls, path_ifc=""):
def load_project(cls, path_ifc: str = "") -> None:
"""Clear and load an ifc project"""
if path_ifc:
@@ -248,7 +273,7 @@ class IfcGit:
bpy.ops.object.select_all(action="DESELECT")
@classmethod
def branches_by_hexsha(cls, repo):
def branches_by_hexsha(cls, repo: git.Repo) -> dict[str, Any]:
"""reverse lookup for branches"""
result = {}
@@ -267,7 +292,7 @@ class IfcGit:
return result
@classmethod
def tags_by_hexsha(cls, repo):
def tags_by_hexsha(cls, repo: git.Repo) -> dict[str, Any]:
"""reverse lookup for tags"""
result = {}
@@ -279,7 +304,7 @@ class IfcGit:
return result
@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"""
"""step-ids of modified, added and removed entities"""
@@ -309,7 +334,7 @@ class IfcGit:
}
@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
props = bpy.context.scene.IfcGitProperties
@@ -341,7 +366,7 @@ class IfcGit:
return step_ids
@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()
modified_shape_object_step_ids = {"modified": []}
@@ -353,7 +378,7 @@ class IfcGit:
return modified_shape_object_step_ids
@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["added"] = step_ids["added"]
@@ -362,7 +387,7 @@ class IfcGit:
return final_step_ids
@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.spaces[0].shading.color_type = "OBJECT"
bpy.ops.object.select_all(action="DESELECT")
@@ -384,7 +409,7 @@ class IfcGit:
obj.color = (1.0, 1.0, 1.0, 0.5)
@classmethod
def switch_to_revision_item(cls):
def switch_to_revision_item(cls) -> None:
props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index]
@@ -399,13 +424,13 @@ class IfcGit:
repo.git.checkout(item.hexsha)
@classmethod
def delete_collection(cls, blender_collection):
def delete_collection(cls, blender_collection: bpy.types.Collection) -> None:
for obj in blender_collection.objects:
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(blender_collection)
@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"""
if not cls.is_valid_ref_format(new_branch_name):
return False
@@ -414,7 +439,7 @@ class IfcGit:
return True
@classmethod
def config_ifcmerge(cls):
def config_ifcmerge(cls) -> None:
config_reader = IfcGitRepo.repo.config_reader()
section = 'mergetool "ifcmerge"'
if not config_reader.has_section(section):
@@ -428,7 +453,7 @@ class IfcGit:
config_writer.set_value(section, "trustExitCode", True)
@classmethod
def config_push(cls, repo):
def config_push(cls, repo: git.Repo) -> None:
"""Set push.autoSetupRemote"""
config_reader = repo.config_reader()
if not config_reader.has_section("push"):
@@ -437,7 +462,7 @@ class IfcGit:
config_writer.set_value("push", "autoSetupRemote", True)
@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"""
path_attributes = os.path.join(repo.git_dir, "info", "attributes")
if not os.path.exists(path_attributes):
@@ -446,7 +471,7 @@ class IfcGit:
f.write("*.ifc text")
@classmethod
def dos2unix(cls, path_file):
def dos2unix(cls, path_file: str) -> None:
with open(path_file, "rb") as infile:
content = infile.read()
with open(path_file, "wb") as output:
@@ -454,7 +479,7 @@ class IfcGit:
output.write(line + b"\n")
@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
repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index]
@@ -498,7 +523,7 @@ class IfcGit:
cls.refresh_revision_list(path_ifc)
@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"""
repo = IfcGitRepo.repo
if not repo:
@@ -514,4 +539,4 @@ class IfcGit:
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(
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.set_manual_blender_offset(ifc_file)
+53 -32
View File
@@ -114,10 +114,10 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod
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]:
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
@@ -392,10 +392,11 @@ class Spatial(blenderbim.core.tool.Spatial):
continue
old_mesh = obj.data
assert isinstance(old_mesh, bpy.types.Mesh)
if visible_element.HasOpenings:
new_mesh = cls.get_gross_mesh_from_element(visible_element)
else:
new_mesh = obj.data.copy()
new_mesh = old_mesh.copy()
obj.data = new_mesh
# Boundary objects are likely triangulated. If a triangulated quad
@@ -463,15 +464,15 @@ class Spatial(blenderbim.core.tool.Spatial):
return mesh
@classmethod
def get_x_y_z_h_mat_from_active_obj(cls, active_obj: bpy.types.Object) -> tuple[float, float, float, float, Matrix]:
mat = active_obj.matrix_world
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
def get_x_y_z_h_mat_from_obj(cls, obj: bpy.types.Object) -> tuple[float, float, float, float, Matrix]:
mat = obj.matrix_world
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
x = global_bbox_center.x
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
@classmethod
@@ -489,9 +490,15 @@ class Spatial(blenderbim.core.tool.Spatial):
selected_objects = bpy.context.selected_objects
boundary_elements = cls.get_boundary_elements(selected_objects)
polys = cls.get_polygons(boundary_elements)
converted_tolerance = cls.get_converted_tolerance(tolerance=0.03)
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2)
union = cls.get_purged_inner_holes_poly(union_geom=union, min_area=cls.get_converted_tolerance(tolerance=0.1))
converted_tolerance = cls.get_converted_tolerance(tolerance_si=0.03)
union = shapely.ops.unary_union(polys).buffer(
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
@classmethod
@@ -521,28 +528,20 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod
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]
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bbox_ws = [obj.matrix_world @ Vector(v) / si_conversion for v in obj.bound_box]
return {
"low_left": (x_values[0], y_values[0]),
"high_left": (x_values[3], y_values[3]),
"low_right": (x_values[4], y_values[4]),
"high_right": (x_values[7], y_values[7]),
"low_left": (bbox_ws[0].x, bbox_ws[0].y),
"high_left": (bbox_ws[3].x, bbox_ws[3].y),
"low_right": (bbox_ws[4].x, bbox_ws[4].y),
"high_right": (bbox_ws[7].x, bbox_ws[7].y),
}
@classmethod
def get_converted_tolerance(cls, tolerance: float) -> float:
def get_converted_tolerance(cls, tolerance_si: float) -> float:
model = tool.Ifc.get()
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
prefix = getattr(project_unit, "Prefix", None)
return ifcopenshell.util.unit.convert(
value=tolerance,
from_prefix=None,
from_unit="METRE",
to_prefix=prefix,
to_unit=project_unit.Name,
)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(model)
return tolerance_si / si_conversion
@classmethod
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
def get_buffered_poly_from_linear_ring(cls, linear_ring: shapely.LinearRing) -> Polygon:
poly = Polygon(linear_ring)
converted_tolerance = cls.get_converted_tolerance(tolerance=0.03)
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style=2, join_style=2)
converted_tolerance = cls.get_converted_tolerance(tolerance_si=0.03)
poly = poly.buffer(
converted_tolerance,
single_sided=True,
cap_style=shapely.BufferCapStyle.flat,
join_style=shapely.BufferJoinStyle.mitre,
)
return poly
@classmethod
@@ -587,8 +591,10 @@ class Spatial(blenderbim.core.tool.Spatial):
bm.edges.index_update()
mat_invert = mat.inverted()
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
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[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):
results.append(element)
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 False
@classmethod
def remove_object(cls, obj: bpy.types.Object) -> None:
bpy.data.objects.remove(obj)
@classmethod
def run_geometry_add_representation(
cls,
+48 -17
View File
@@ -22,6 +22,7 @@ import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell.api.sequence
from typing import Any, Dict, Optional
import time
import socket
import sys
import os
@@ -122,7 +123,8 @@ class Web(blenderbim.core.tool.Web):
blenderbim_lib_path = os.path.join(blenderbim_path, "libs", "site", "packages")
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(
[sys.executable, ws_path, "--p", str(port), "--host", "127.0.0.1"],
@@ -130,19 +132,6 @@ class Web(blenderbim.core.tool.Web):
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)
@classmethod
@@ -223,12 +212,29 @@ class Web(blenderbim.core.tool.Web):
with open(pid_file, "w") as f:
json.dump(pids, f, indent=4)
ws_process.terminate()
ws_process.wait()
ws_process.kill()
ws_process = None
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
def send_webui_data(
@@ -279,6 +285,8 @@ class Web(blenderbim.core.tool.Web):
cls.handle_csv_operator(operator["operator"])
elif operator["sourcePage"] == "gantt":
cls.handle_gantt_operator(operator["operator"])
elif operator["sourcePage"] == "drawings":
cls.handle_drawings_operator(operator["operator"])
return 1.0
@classmethod
@@ -323,6 +331,29 @@ class Web(blenderbim.core.tool.Web):
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")
@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
def open_web_browser(cls, port: int) -> None:
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
bcf
ifcclash
bimtester
ifccobie
ifccsv
ifcdiff
@@ -172,7 +171,6 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
elementpath
six
lark-parser
behave
parse
parse_type
xlsxwriter
@@ -184,8 +182,7 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
Notes:
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>`__.
3. ``ifcjson`` can be found `here <https://github.com/IFCJSON-Team/IFC2JSON_python/tree/master/file_converters>`__.
2. ``ifcjson`` can be found `here <https://github.com/IFCJSON-Team/IFC2JSON_python/tree/master/file_converters>`__.
Required static assets are:
::
-1
View File
@@ -2,7 +2,6 @@
markers =
aggregate
attribute
bimtester
brick
classification
context
@@ -22,9 +22,7 @@ Scenario: Assign object
And I press "bim.enable_editing_aggregate"
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})"
Then the object "IfcSite/My Site" is in the collection "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"
Then the object "IfcBuildingStorey/My Storey" is aggregated by object "IfcSite/My Site"
Scenario: Unassign object
Given an empty IFC project
@@ -34,29 +32,7 @@ Scenario: Unassign object
And I press "bim.aggregate_assign_object(relating_object={relating_object})"
And the object "IfcBuildingStorey/My Storey" is selected
When I press "bim.aggregate_unassign_object"
Then the object "IfcSite/My Site" is in the collection "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 "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"
Then the object "IfcBuildingStorey/My Storey" has no aggregate
Scenario: Add aggregate
Given an empty IFC project
@@ -67,23 +43,9 @@ Scenario: Add aggregate
And I press "bim.assign_class"
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 - 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"
Then the object "IfcElementAssembly/Default_Name" exists
And the object "IfcWall/Cube" is aggregated by object "IfcElementAssembly/Default_Name"
And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
Scenario: Add aggregate - add a nested aggregate
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_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"
And the object "IfcWall/Cube" is selected
And I press "bim.add_aggregate"
Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly.001"
And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001"
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly"
And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey"
And I press "bim.add_aggregate(aggregate_name='Default_Name2')"
Then the object "IfcElementAssembly/Default_Name" exists
And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey"
And the object "IfcElementAssembly/Default_Name2" exists
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
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 the object "IfcMember/Cube" is selected
And additionally the object "IfcCovering/Cube" is selected
When I press "bim.add_aggregate(ifc_class='IfcWall')"
Then the object "IfcMember/Cube" is in the collection "IfcWall/Assembly"
And the object "IfcCovering/Cube" is in the collection "IfcWall/Assembly"
And the object "IfcWall/Assembly" is in the collection "IfcWall/Assembly"
And the collection "IfcWall/Assembly" is in the collection "IfcBuildingStorey/My Storey"
When I press "bim.add_aggregate(ifc_class='IfcWall', aggregate_name='Assembly')"
Then the object "IfcMember/Cube" is aggregated by object "IfcWall/Assembly"
And the object "IfcCovering/Cube" is aggregated by object "IfcWall/Assembly"
And the object "IfcWall/Assembly" is contained in object "IfcBuildingStorey/My Storey"
@@ -4,34 +4,34 @@ Feature: Attribute
Scenario: Enable editing attributes
Given an empty IFC project
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
Scenario: Disable editing attributes
Given an empty IFC project
And the object "IfcSite/My Site" is selected
And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')"
When I press "bim.disable_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')"
Then nothing happens
Scenario: Edit attributes
Given an empty IFC project
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"
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"
And the object "IfcSite/My Site" does not exist
Scenario: Edit attributes - longitude / latitude
Given an empty IFC project
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"
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.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')"
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')"
And I press "bim.edit_attributes(obj='IfcSite/My Site')"
Then nothing happens
Scenario: Copy attribute to selected
@@ -48,7 +48,7 @@ Scenario: Copy attribute to selected
And I press "bim.assign_class"
And the object "IfcWall/Cube" 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"
When I press "bim.copy_attribute_to_selection(name='Description')"
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 the object "IfcWall/Cube" is selected
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')"
Then nothing happens
@@ -8,8 +8,36 @@ Scenario: Execute generate flooring coverings from walls
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
# 1st wall
And I press "bim.add_constr_type_instance"
And the object "IfcWall/Wall" is selected
When I press "bim.add_instance_flooring_coverings_from_walls"
Then nothing happens
And I press "bim.change_layer_length(length=3.6)"
# 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 I set "scene.DocProperties.active_drawing_index" to "0"
And I press "bim.activate_drawing(drawing={drawing})"
And the object "IfcAnnotation/PLAN_VIEW" is selected
When I press "bim.override_object_delete"
When the object "IfcAnnotation/PLAN_VIEW" is selected
And I press "bim.override_object_delete"
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 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)"
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"
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 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)"
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"
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_class" to "IfcWall"
And I press "bim.assign_class"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
And I press "bim.remove_representation(representation_id={representation})"
When the variable "representation_body" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
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
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 the object "IfcWallType/Cube" is selected
When the variable "representation" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()"
And I press "bim.remove_representation(representation_id={representation})"
When the variable "representation_body" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()"
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 "IfcWall/Wall" 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 the object "IfcWall/Wall" is selected
When the variable "representation" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()"
And I press "bim.remove_representation(representation_id={representation})"
When the variable "representation_body" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()"
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 "IfcWall/Wall" 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 I press "bim.assign_style_to_selected(style_id={style})"
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 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"
@@ -427,18 +433,14 @@ Scenario: Override duplicate move - copying an aggregate
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
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"
When I press "bim.add_aggregate(aggregate_name='Assembly')"
When the object "IfcWall/Cube" is selected
And additionally the object "IfcElementAssembly/Assembly" is selected
When I duplicate the selected objects
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" is in the collection "IfcElementAssembly/Assembly.001"
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey"
And the object "IfcWall/Cube.001" is aggregated by object "IfcElementAssembly/Assembly.001"
And the object "IfcElementAssembly/Assembly.001" is contained in object "IfcBuildingStorey/My Storey"
Scenario: Override duplicate move - copying objects with connection
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" bottom left corner is at "0,0,0"
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}"
When I press "bim.add_constr_type_instance"
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
And the object "IfcWall/Wall_02.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')"
Then the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default"
When I press "bim.unassign_material"
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 the object "IfcWall/Cube" is selected
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)"
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="ifcsverchok")"
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/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
Given an empty IFC project
And I load the demo construction library
@@ -272,7 +264,7 @@ Scenario: Add a slab
Given an empty IFC project
And I load the demo construction library
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}"
When I press "bim.hotkey(hotkey='S_A')"
Then the object "IfcSlab/Slab" is an "IfcSlab"
@@ -284,7 +276,7 @@ Scenario: Enable editing a slab profile
Given an empty IFC project
And I load the demo construction library
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 press "bim.hotkey(hotkey='S_A')"
And the object "IfcSlab/Slab" is selected
@@ -297,7 +289,7 @@ Scenario: Disable editing a slab profile
Given an empty IFC project
And I load the demo construction library
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 press "bim.hotkey(hotkey='S_A')"
And the object "IfcSlab/Slab" is selected
@@ -311,7 +303,7 @@ Scenario: Edit a slab profile
Given an empty IFC project
And I load the demo construction library
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 press "bim.hotkey(hotkey='S_A')"
And the object "IfcSlab/Slab" is selected
+10 -10
View File
@@ -833,31 +833,31 @@ Scenario: Unlink IFC
Scenario: Export IFC - blank 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
Scenario: Export IFC - with basic contents
Given an empty Blender session
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"
Scenario: Export IFC - with basic contents and saving as another file
Given an empty Blender session
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"
Scenario: Export IFC - with basic contents and saving as IfcJSON where import is not supported
Given an empty Blender session
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"
Scenario: Export IFC - with basic contents and round-tripping an IfcZip
Given an empty Blender session
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"
When an empty Blender session is started
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
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')"
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"
Scenario: Export IFC - with deleted objects synchronised
Given an empty IFC project
When the object "IfcBuildingStorey/My Storey" is selected
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 I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
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
Given an empty IFC project
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 I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
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
And I press "mesh.add_grid"
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 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"
@@ -905,7 +905,7 @@ Scenario: Export IFC - with changed object scale synchronised
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
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 I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
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
Given an empty IFC project
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 "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')"
# Testing Q_LENGTH type of prop
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397"
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257"
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.156"
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"
+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 the object "Cube" is selected
And additionally the object "Cube.001" is selected
When I set "scene.BIMQtoProperties.qto_methods" to "FORMWORK"
And I press "bim.execute_qto_method"
When I press "bim.calculate_formwork_area"
Then "scene.BIMQtoProperties.qto_result" is "21.5"
Scenario: Execute qto method - side formwork areas
Given an empty Blender session
And I add a cube
And the object "Cube" is selected
When I set "scene.BIMQtoProperties.qto_methods" to "SIDE_FORMWORK"
And I press "bim.execute_qto_method"
When I press "bim.calculate_side_formwork_area"
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
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.calculate_all_quantities"
When I press "bim.perform_quantity_take_off"
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.disable_pset_editing(obj='IfcWall/Cube', obj_type='Object')"
Then "active_object.PsetProperties.qto_name" is "Qto_WallBaseQuantities"
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is "2000.0"
Then "active_object.PsetProperties.active_pset_name" is "Qto_WallBaseQuantities"
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2000.0"
@@ -32,7 +32,7 @@ Scenario: Add Labor Resource
Given an empty IFC project
When I press "bim.load_resources"
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})"
Then nothing happens
@@ -41,7 +41,7 @@ Scenario: Enable Editing Resource
Given an empty IFC project
When I press "bim.load_resources"
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})"
Then nothing happens
@@ -49,7 +49,7 @@ Scenario: Edit Resource
Given an empty IFC project
When I press "bim.load_resources"
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})"
And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo"
When I press "bim.edit_resource()"
@@ -59,7 +59,7 @@ Scenario: Remove Resource
Given an empty IFC project
When I press "bim.load_resources"
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})"
And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo"
When I press "bim.edit_resource()"
@@ -70,9 +70,9 @@ Scenario: Remove Parent Resource
Given an empty IFC project
When I press "bim.load_resources"
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})"
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})"
And I set "scene.BIMResourceProperties.resource_time_attributes.get('Name').string_value" to "TimelyFoo"
When I press "bim.edit_resource_time()"
@@ -83,9 +83,9 @@ Scenario: Enable Editing Resource time
Given an empty IFC project
When I press "bim.load_resources"
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})"
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})"
Then nothing happens
@@ -93,9 +93,9 @@ Scenario: Disable editing resource time
Given an empty IFC project
When I press "bim.load_resources"
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})"
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})"
When I press "bim.disable_editing_resource()"
Then nothing happens
@@ -104,9 +104,9 @@ Scenario: Edit Resource time
Given an empty IFC project
When I press "bim.load_resources"
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})"
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})"
And I set "scene.BIMResourceProperties.resource_time_attributes.get('ScheduleUsage').float_value" to "305.25"
When I press "bim.edit_resource_time()"
@@ -116,9 +116,9 @@ Scenario: Enable Editing Resource Costs
Given an empty IFC project
When I press "bim.load_resources"
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})"
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})"
Then nothing happens
@@ -126,9 +126,9 @@ Scenario: Disable Editing Resource Costs
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.disable_editing_resource()"
Then nothing happens
@@ -138,9 +138,9 @@ Scenario: Add Resource Fixed Cost Value
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.add_cost_value(parent={labor_resource}, cost_type="FIXED")"
Then nothing happens
@@ -149,12 +149,12 @@ Scenario: Edit Resource Cost Value
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.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})"
Then nothing happens
@@ -162,12 +162,12 @@ Scenario: Enable Editing Resource Cost Value Formula
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.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})"
Then nothing happens
@@ -175,12 +175,12 @@ Scenario: Edit Resource Cost Value Formula
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.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})"
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})"
@@ -190,12 +190,12 @@ Scenario: Edit Resource Cost Value
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.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})"
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})"
@@ -205,11 +205,11 @@ Scenario: Enable Editing Resource Quantity
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.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})"
Then nothing happens
@@ -217,27 +217,26 @@ Scenario: Edit Resource Quantity
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.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})"
And I set "scene.BIMResourceProperties.quantity_attributes.get('AreaValue').float_value" to "50.00"
And the variable "quantity_area" is "IfcStore.get_file().by_type('IfcQuantityArea')[0].id()"
When I press "bim.edit_resource_quantity(physical_quantity={quantity_area})"
And I set "scene.BIMResourceProperties.quantity_attributes.get('TimeValue').float_value" to "50.00"
And the variable "quantity_time" is "IfcStore.get_file().by_type('IfcQuantityTime')[0].id()"
When I press "bim.edit_resource_quantity(physical_quantity={quantity_time})"
Then nothing happens
Scenario: Remove Resource Quantity
Given an empty IFC project
When I press "bim.load_resources"
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})"
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.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityArea")"
And the variable "quantity_area" is "IfcStore.get_file().by_type('IfcQuantityArea')[0].id()"
When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityTime")"
When I press "bim.remove_resource_quantity(resource={labor_resource})"
Then nothing happens
@@ -245,9 +244,9 @@ Scenario: Add Productivity data
Given an empty IFC project
When I press "bim.load_resources"
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})"
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 press "bim.add_productivity_data"
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')"
When I press "bim.load_resources"
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})"
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 press "bim.add_productivity_data"
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_consumed[0].hours" to "5"
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})"
Then nothing happens
Then nothing happens
Scenario: Assign Resource
Given an empty IFC project
When I press "bim.load_resources"
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})"
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 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 selected
And I press "bim.assign_resource(resource={labor_resource}, related_object="")"
And I press "bim.assign_resource(resource={labor_resource})"
Then nothing happens
Scenario: UnAssign Resource
Scenario: Unassign Resource
Given an empty IFC project
When I press "bim.load_resources"
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})"
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 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 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 I press "bim.unassign_resource(resource={labor_resource}, related_object="")"
Then nothing happens
And I press "bim.unassign_resource(resource={labor_resource})"
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 press "bim.assign_class"
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"
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"
Then the object "IfcSpace/Cube" is an "IfcSpace"
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"
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
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 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 press "bim.add_filter(index=0, type='entity', module='search')"
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
+15 -12
View File
@@ -27,7 +27,8 @@ Scenario: Assign container
And the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container"
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"
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_class" to "IfcWall"
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"
When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True"
And I press "bim.copy_to_container"
When I press "bim.copy_to_container"
Then the object "IfcWall/Cube.001" is in the collection "IfcSite/My Site"
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_class" to "IfcWall"
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"
When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True"
And I press "bim.reference_structure"
When I press "bim.reference_structure"
Then nothing happens
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_class" to "IfcWall"
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"
When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True"
And I press "bim.reference_structure"
When I press "bim.reference_structure"
And I press "bim.dereference_structure"
Then nothing happens
@@ -80,7 +81,8 @@ Scenario: Select container
And the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container"
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"
Then nothing happens
@@ -94,7 +96,8 @@ Scenario: Select similar container
And the object "IfcWall/Cube" is selected
And I press "bim.enable_editing_container"
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"
Then nothing happens
@@ -75,7 +75,6 @@ Scenario: Disable editing style
Scenario: Load styles
Given an empty IFC project
And I press "bim.add_style"
When I press "bim.load_styles(style_type='IfcSurfaceStyle')"
Then nothing happens
+34 -3
View File
@@ -684,17 +684,17 @@ def prop_is_roughly_value(prop, value):
value = replace_variables(value)
is_value = False
try:
exec(f'assert round(bpy.context.{prop}, 5) == "{value}"')
exec(f'assert round(bpy.context.{prop}, 3) == "{value}"')
is_value = True
except:
try:
exec(f"assert round(bpy.context.{prop}, 5) == {value}")
exec(f"assert round(bpy.context.{prop}, 3) == {value}")
is_value = True
except:
pass
if not is_value:
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}"
@@ -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}'
@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}"'))
def the_file_name_should_contain_value(name, value):
name = replace_variables(name)
@@ -36,6 +36,7 @@ class TestAssignObject:
def test_run(self, ifc, aggregate, collector):
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")
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.run(
"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")
geometry.clear_cache("element").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")
ifc.run("geometry.edit_object_placement", product="element", matrix="matrix").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):
geometry.is_edited("obj").should_be_called().will_return(False)
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.get_representation_data("representation").should_be_called().will_return(None)
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):
geometry.is_edited("obj").should_be_called().will_return(False)
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.get_representation_data("representation").should_be_called().will_return("existing_data")
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):
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.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.get_representation_data("representation").should_be_called().will_return("data")
geometry.change_object_data("obj", "data", is_global=True).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()
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")
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()
subject.switch_representation(
ifc,
@@ -273,6 +298,7 @@ class TestSwitchRepresentation:
geometry.run_geometry_update_representation(obj="obj").should_be_called()
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.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.get_representation_data("representation").should_be_called().will_return("data")
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
from test.core.bootstrap import ifc, project
from test.core.bootstrap import ifc, project, spatial, georeference
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")
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):
project.run_context_add_context(
@@ -67,7 +67,7 @@ class TestCreateProject:
context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent="plan"
).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.run("project.create_file", version="IFC4").should_be_called().will_return("ifc")
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.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.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, 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.run("project.create_file", version="IFC4").should_be_called().will_return("ifc")
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.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.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, 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.run("project.create_file", version="IFC2X3").should_be_called().will_return("ifc")
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.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.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, 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",
)
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)
root.get_object_name("obj").should_be_called().will_return("name")
ifc.run(
@@ -127,7 +129,11 @@ class TestAssignClass:
root.run_geometry_add_representation(
obj="obj", context="context", ifc_representation_class="ifc_representation_class", profile_set_usage=None
).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()
subject.assign_class(
ifc,
@@ -141,7 +147,9 @@ class TestAssignClass:
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)
root.get_object_name("obj").should_be_called().will_return("name")
ifc.run(
@@ -149,7 +157,39 @@ class TestAssignClass:
).should_be_called().will_return("element")
root.set_object_name("obj", "element").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()
subject.assign_class(
ifc,
+6 -17
View File
@@ -38,24 +38,21 @@ class TestDereferenceStructure:
class TestAssignContainer:
def test_run(self, ifc, collector, spatial):
spatial.can_contain("structure_obj", "element_obj").should_be_called().will_return(True)
ifc.get_entity("structure_obj").should_be_called().will_return("structure")
spatial.can_contain("container", "element_obj").should_be_called().will_return(True)
ifc.get_entity("element_obj").should_be_called().will_return("element")
ifc.run(
"spatial.assign_container", products=["element"], relating_structure="structure"
"spatial.assign_container", products=["element"], relating_structure="container"
).should_be_called().will_return("rel")
spatial.disable_editing("element_obj").should_be_called()
collector.assign("element_obj").should_be_called()
assert (
subject.assign_container(ifc, collector, spatial, structure_obj="structure_obj", element_obj="element_obj")
== "rel"
subject.assign_container(ifc, collector, spatial, container="container", element_obj="element_obj") == "rel"
)
class TestEnableEditingContainer:
def test_run(self, spatial):
spatial.enable_editing("obj").should_be_called()
spatial.import_containers().should_be_called()
subject.enable_editing_container(spatial, obj="obj")
@@ -65,12 +62,6 @@ class TestDisableEditingContainer:
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:
def test_run(self, ifc, collector):
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.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_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()
@@ -105,7 +96,7 @@ class TestCopyToContainer:
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.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()
@@ -114,11 +105,9 @@ class TestCopyToContainer:
class TestSelectContainer:
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")
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:
+11 -6
View File
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blenderbim.core.type as subject
from test.core.bootstrap import ifc, type
from test.core.bootstrap import ifc, type, geometry
class TestAssignType:
@@ -42,11 +42,16 @@ class TestAssignType:
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_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.unlink(element="element_type").should_be_called()
type.remove_object("obj").should_be_called()
subject.purge_unused_types(ifc, type)
geometry.delete_ifc_object("obj").should_be_called()
subject.purge_unused_types(ifc, type, geometry)
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_obj = bpy.data.objects.new("Object", None)
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):
ifc = ifcopenshell.file(schema="IFC2X3")
@@ -51,7 +51,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None)
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):
ifc = ifcopenshell.file()
@@ -62,7 +62,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None)
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):
structure_obj = bpy.data.objects.new("Object", None)
@@ -78,7 +78,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None)
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):
ifc = ifcopenshell.file()
@@ -89,7 +89,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcTask()
element_obj = bpy.data.objects.new("Object", None)
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):
ifc = ifcopenshell.file()
@@ -100,7 +100,7 @@ class TestCanContain(NewFile):
element = ifc.createIfcGrid()
element_obj = bpy.data.objects.new("Object", None)
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):
@@ -181,46 +181,6 @@ class TestGetRelativeObjectMatrix(NewFile):
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):
def test_nothing(self):
pass
-7
View File
@@ -168,13 +168,6 @@ class TestHasMaterialUsage(NewFile):
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):
def test_nothing(self):
pass
+3 -1
View File
@@ -22,4 +22,6 @@ include ../common.mk
.PHONY: 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.api
import ifcopenshell.api.pset
import ifcopenshell.util.element
import ifcopenshell.util.unit
import ifcopenshell.util.selector
import multiprocessing
from collections import namedtuple
from typing import Any
Function = namedtuple("Function", ["measure", "name", "description"])
@@ -47,7 +49,7 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst
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 name, quantities in qtos.items():
qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False)
@@ -59,7 +61,7 @@ def edit_qtos(ifc_file, results) -> None:
class SI2ProjectUnitConverter:
def __init__(self, ifc_file):
def __init__(self, ifc_file: ifcopenshell.file):
self.project_units = {
"IfcAreaMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "AREAUNIT"),
"IfcLengthMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT"),
@@ -391,6 +391,31 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
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) {
// 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::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::matrix4::ptr, gp_GTrsf&);
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::shell::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::loft::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
std::ostringstream oss;
face->print(oss);
@@ -382,7 +382,11 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
}
}
} 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()) {
// Is this necessary
@@ -434,10 +438,20 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
for (; jt.More(); jt.Next()) {
Message_ListIteratorOfListOfMsg kt(jt.Value());
for (; kt.More(); kt.Next()) {
char* c = new char[kt.Value().Value().LengthOfCString() + 1];
kt.Value().Value().ToUTF8CString(c);
Logger::Notice(c, face->instance);
char* c = new char[kt.Value().Original().LengthOfCString() + 1];
kt.Value().Original().ToUTF8CString(c);
std::string message = 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:
ifcopenshell/installation
ifcopenshell/getting_started
ifcopenshell/geometry_iterator
ifcopenshell/geometry_settings
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/>.
import ifcopenshell
from typing import Optional, Literal
import ifcopenshell.util.representation
from typing import Optional
def add_context(
file: ifcopenshell.file,
context_type: Optional[Literal["Model", "Plan"]] = None,
context_identifier: Optional[str] = None,
target_view: Optional[str] = None,
context_type: Optional[ifcopenshell.util.representation.CONTEXT_TYPE] = None,
context_identifier: Optional[ifcopenshell.util.representation.REPRESENTATION_IDENTIFIER] = None,
target_view: Optional[ifcopenshell.util.representation.TARGET_VIEW] = None,
parent: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new geometric representation context
@@ -106,7 +107,6 @@ def add_context(
the common target views above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional
for contexts, but mandatory for subcontexts.
:type target_view: str, optional
:param parent: the parent context. Must be left as None (the default)
for contexts, and only set for subcontexts. Note that there are only
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):
if attr_index == 0xFFFFFFFF:
invalid_attrs.append(attr_name)
raise ValueError(
"entity instance of type '%s' doesn't have the following attributes: %s."
% (e.is_a(True), ", ".join(invalid_attrs))
)
raise ValueError(
"entity instance of type '%s' doesn't have the following attributes: %s."
% (e.is_a(True), ", ".join(invalid_attrs))
)
# Restore transaction status
if attrs:
@@ -41,6 +41,7 @@ except ImportError:
class shape_tuple(NamedTuple):
"""A tuple containing IfcOpenShell serialized element/shape and pythonOCC shape."""
data: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization]
geometry: TopoDS.TopoDS_Shape
styles: tuple[tuple[float, float, float, float], ...]
+5
View File
@@ -26,6 +26,7 @@ import typing
import inspect
import collections
import importlib
import importlib.util
from typing import Union
@@ -177,6 +178,10 @@ def _extract_docs(cls, method_name, boilerplate_args):
param_name = line.split(":")[1].strip().replace("param ", "")
if param_name in inputs:
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:
description += line
elif i > 2:
@@ -17,12 +17,15 @@
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.pset
import ifcopenshell.util.element
from logging import Logger
from typing import Union
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
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
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
be stored in. This should be a standard name that is one of the
quantity names of a buildingSMART quantity template. For example, it
may be "NetSideArea" for walls, which exists in
Qto_WallBaseQuantities. The quantity set name will be based on the
standard buildingSMART quantity template.
:type quantity_name: str
Example:
@@ -63,7 +64,7 @@ class Patcher:
self.source_property_name = property_name
self.destination_quantity_name = quantity_name
def patch(self):
def patch(self) -> None:
self.qto_template_cache = {}
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")],
)
def process_product(self, product, definitions):
def process_product(
self, product: ifcopenshell.entity_instance, definitions: list[ifcopenshell.entity_instance]
) -> None:
value = None
has_quantity = False
qtos = {}
@@ -98,12 +101,12 @@ class Patcher:
"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):
if self.destination_quantity_name in [t.Name for t in template.HasPropertyTemplates]:
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:
self.qto_template_cache[ifc_class] = self.psetqto.get_applicable(ifc_class, qto_only=True)
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
first. The first model is already specified as the input to
IfcPatch.
:type filepath: Union[str, ifcopenshell.file]
:filter_glob filepath: *.ifc;*.ifczip;*.ifcxml
Example:
@@ -53,11 +55,15 @@ class Patcher:
self.filepath = filepath
def patch(self):
if isinstance(self.filepath, ifcopenshell.file):
other = self.filepath
else:
other = ifcopenshell.open(self.filepath)
for filepath in self.filepath:
if isinstance(filepath, ifcopenshell.file):
other = 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):
other = ifcopenshell.util.unit.convert_file_length_units(other, main_unit)
@@ -20,10 +20,23 @@ import math
import numpy as np
import ifcopenshell
import ifcopenshell.util.placement
import typing
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
Every physical object in an IFC model has an object placement, a
@@ -41,11 +54,11 @@ class Patcher:
entire IFC model.
: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.
:type y: float
:type y: typing.Union[str, float]
: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
translate, or to first translate and rotate afterwards. Defaults to
rotate first then translate.
@@ -55,13 +68,13 @@ class Patcher:
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
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
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
axis. Angles are in decimal degrees and positive is anticlockwise.
:type az: float,optional
:type az: typing.Union[str, float],optional
Example:
@@ -79,13 +92,21 @@ class Patcher:
self.src = src
self.file = file
self.logger = logger
self.x = x
self.y = y
self.z = z
self.x = float(x)
self.y = float(y)
self.z = float(z)
self.should_rotate_first = should_rotate_first
self.ax = ax
self.ay = ay
self.az = az
self.ax = None
self.ay = None
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:
self.angle_type = None
elif self.ay is None:
@@ -16,15 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
import typing
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
All objects placed relative to the storeys will also be shifted.
:param z: The Z value in project length units to offset storeys by.
:type z: float
:type z: typing.Union[str, float]
Example:
@@ -36,7 +38,7 @@ class Patcher:
self.src = src
self.file = file
self.logger = logger
self.z = z
self.z = float(z)
def patch(self):
project = self.file.by_type("IfcProject")[0]
+17 -16
View File
@@ -19,6 +19,7 @@
import ifcopenshell
import ifcopenshell.api.georeference
from ifcpatch.recipes import OffsetObjectPlacements, SetWorldCoordinateSystem
import typing
class Patcher:
@@ -28,14 +29,14 @@ class Patcher:
file,
logger,
name: str = "EPSG:1234",
x: float = 0,
y: float = 0,
z: float = 0,
e: float = 0,
n: float = 0,
h: float = 0,
gn_angle: float = 0,
rotate_angle: float = 0,
x: typing.Union[str, float] = "0",
y: typing.Union[str, float] = "0",
z: typing.Union[str, float] = "0",
e: typing.Union[str, float] = "0",
n: typing.Union[str, float] = "0",
h: typing.Union[str, float] = "0",
gn_angle: typing.Union[str, float] = "0",
rotate_angle: typing.Union[str, float] = "0",
):
"""Sets a false origin with a map conversion in a model
@@ -62,14 +63,14 @@ class Patcher:
self.file = file
self.logger = logger
self.name = name
self.x = x
self.y = y
self.z = z
self.e = e
self.n = n
self.h = h
self.gn_angle = gn_angle
self.rotate_angle = rotate_angle
self.x = float(x)
self.y = float(y)
self.z = float(z)
self.e = float(e)
self.n = float(n)
self.h = float(h)
self.gn_angle = float(gn_angle)
self.rotate_angle = float(rotate_angle)
def patch(self):
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