Compare commits

...

30 Commits

Author SHA1 Message Date
Dion Moult 9a14406f92 BIM tool can now add parametric profiles for beams, not just columns. 2021-07-18 14:48:30 +10:00
Dion Moult 32062e4085 Experimental WIP broken Sverchok API node. 2021-07-18 12:58:27 +10:00
Dion Moult 8da17cf4a0 New extract docs system to dynamically parse usecases. 2021-07-18 12:57:38 +10:00
Dion Moult 2d6e941c41 New documentation for project create_file usecase 2021-07-18 12:56:49 +10:00
Dion Moult 0fffbac519 Fix #1569. 2021-07-18 09:26:37 +10:00
Dion Moult db62faa5ae Fix #1570. 2021-07-18 09:21:38 +10:00
Dion Moult 02dc7b5274 The align tool now works on any generic product, not just walls. 2021-07-17 21:36:03 +10:00
Dion Moult 1bf136e93c Copying a spatial element now also automatically creates a new collection. See #1565. 2021-07-17 18:58:25 +10:00
Dion Moult cccab9e751 Minor fix. See #1567. 2021-07-17 18:38:04 +10:00
Thomas Krijnen 99e74533c3 #1567 Clear inverse cache map after entity insertion 2021-07-17 10:20:50 +02:00
Thomas Krijnen 0f39b09396 #1567 add instance after the values are set 2021-07-17 09:38:31 +02:00
Dion Moult ac2324a65f Minor fix 2021-07-17 17:28:27 +10:00
Thomas Krijnen ca85927a51 #1572 wrong serializer output temp name 2021-07-17 09:06:03 +02:00
Prabhat Singh e3786423c2 Update README.md (#1571) 2021-07-17 16:42:19 +10:00
Dion Moult 8d352ea07a Fix bug where editing a single material didn't quite work. 2021-07-17 09:13:37 +10:00
Dion Moult fd04066940 Minor fix 2021-07-15 18:21:18 +10:00
Dion Moult d34956c2c7 Add readme info on how to use new BCFAPI. Thanks TestPrab! 2021-07-15 18:02:16 +10:00
Prabhat Singh ad2585c571 Merge branch 'v0.6.0' of https://github.com/TestPrab/IfcOpenShell into prabhat_gsoc (#1566) 2021-07-15 17:41:24 +10:00
Dion Moult a08cf23d63 Fix #1562. Fix bug where all instances change meshes when changing type. 2021-07-14 09:37:29 +10:00
Dion Moult 37fe04ffd2 New recipe to downgrade indexed poly curves, often used to allow IFC4 grids to show up in viewers like Revizto / XBim Xplorer / etc. 2021-07-13 19:32:12 +10:00
Dion Moult 248b21eab3 Importing 2D elements of any arbitrary element is now supported 2021-07-13 18:58:30 +10:00
Dion Moult 63f578315d Add support for 3D curve / wireframe representations. 2021-07-13 18:45:07 +10:00
Dion Moult 19b6765994 Minor fix 2021-07-13 16:39:56 +10:00
Dion Moult 7456c270ff You can now extend a wall to your cursor location instead of another wall 2021-07-12 22:10:10 +10:00
Dion Moult b480b43435 BIM workspace tool is now no longer wall specific, and adapts to different element types 2021-07-12 17:34:35 +10:00
Dion Moult 3614587556 Fix #1558. New loading bar when importing an IFC. 2021-07-11 10:29:19 +10:00
Dion Moult 86ce367ba8 Fix #1561. 2021-07-11 09:28:22 +10:00
Thomas Krijnen d320c1dac1 Some quick and dirty #1560 2021-07-10 21:43:27 +02:00
Thomas Krijnen cf48661a35 limit schema versions on Travis 2021-07-10 21:10:03 +02:00
Thomas Krijnen 5e03846783 Update .travis.yml 2021-07-10 17:17:47 +02:00
30 changed files with 1108 additions and 144 deletions
+6
View File
@@ -22,10 +22,15 @@ addons:
- nlohmann-json3-dev
- opencollada-dev
- python3-all-dev
- python3-pip
- swig
before_script:
- if [ $TRAVIS_OS_NAME == "linux" ]; then ccache -z; fi
install:
# for IDS
- python3 -m pip install xmlschema
script:
- pwd
@@ -39,6 +44,7 @@ script:
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
"-DSCHEMA_VERSIONS=2x3;4" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
../cmake
+3 -1
View File
@@ -494,7 +494,9 @@ function(files_for_ifc_version IFC_VERSION RESULT_NAME)
)
endfunction()
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4")
if(NOT SCHEMA_VERSIONS)
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4")
endif()
foreach(s ${SCHEMA_VERSIONS})
add_definitions(-DHAS_SCHEMA_${s})
+41 -2
View File
@@ -7,13 +7,13 @@ is available via `bcfapi.py`.
- BCF-XML version 2.1: Fully supported
- BCF-API version 2.1: Not supported, will probably tackle this after BCF-API v3.0
- BCF-XML version 3.0: Almost fully supported, except for the documents module
- BCF-API version 3.0: Not supported, but work underway to support it
- BCF-API version 3.0: Almost fully supported, except for two requests.
## bcfxml
The `bcfxml` module lets you interact with the BCF-XML standard.
```
```python
from bcf import bcfxml
@@ -60,3 +60,42 @@ topic = bcfxml.get_topic(guid)
topic.title = "New title"
bcfxml.edit_topic(topic)
```
## bcfapi
The `bcfapi` module lets you interact with the BCF-API standard.
```python
from bcf.v3.bcfapi import Client
client_id = "YOUR_CLIENT_ID"
client_secret = "YOUR_CLIENT_SECRET"
client = Client(client_id, client_secret)
client.set_urls(base_url="OPENCDE_BASEURL")
auth_methods = client.get_auth_methods()
# Our library currently only implements the authorization_code flow
if "authorization_code" in auth_methods:
client.login()
versions = client.get_versions()
if "3.0" in versions:
client.set_version(version="3.0")
data = client.get_projects()
print(data)
project_id = data[0]["project_id"]
print(project_id)
data = client.get_project(project_id)
print(data)
data = client.get_extensions(project_id)
print(data)
```
## Todo List
The remaining work that needs to be completed in `bcfxml.py` and `bcfapi.py`.
* For `bcfxml.py` two xsds support is remaining namely 'documents.xsd` and `extensions.xsd`.
* For `bcfapi.py` two requests that are `get_topics` and `get_comments` are remaining.
+540
View File
@@ -0,0 +1,540 @@
import uuid
import time
import json
import urllib
import requests
import webbrowser
import http.server
import base64
client_id, client_secret = "", ""
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
def do_GET(self):
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
self.server.auth_code = query.get("code", [""])[0]
self.server.auth_state = query.get("state", [""])[0]
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write("You have now authenticated :) You may now close this browser window.".encode("utf-8"))
class Client:
def __init__(self, client_id, client_secret):
self.baseurl = None
self.access_token = ""
self.refresh_token = ""
self.access_token_expires_on = time.time()
self.refresh_token_expires_on = float("inf")
self.auth_endpoint = None
self.token_endpoint = None
self.client_id = client_id
self.client_secret = client_secret
self.version_ids = {}
self.version = None
self.auth_method = None
self.redirect_uri = None
self.api_baseurl = None
def get(self, endpoint, params=None, is_auth_required=False):
headers = {"Authorization": "Bearer " + self.get_access_token()}
return requests.get(f"{self.api_baseurl}{endpoint}", headers=headers, params=params or None).json()
def post(self, endpoint, data=None, params=None):
headers = {
"Authorization": "Bearer " + self.get_access_token(),
"Content-type": "application/json",
}
resp = requests.post(
f"{self.api_baseurl}{endpoint}",
headers=headers,
params=params or None,
data=data or None,
)
return resp.status_code, resp.text
def put(self, endpoint, data=None, params=None):
headers = {
"Authorization": "Bearer " + self.get_access_token(),
"Content-type": "application/json",
}
resp = requests.put(
f"{self.baseurl}{endpoint}",
headers=headers,
params=params or None,
data=data or None,
)
return resp.status_code, resp.text
def set_urls(self, base_url=None, redirect_uri=None):
self.baseurl = base_url
self.redirect_uri = redirect_uri
def delete(self, endpoint, params=None):
headers = {"Authorization": "Bearer " + self.get_access_token()}
resp = requests.put(
f"{self.baseurl}{endpoint}",
headers=headers,
params=params or None,
)
return resp.status_code
def get_access_token(self):
if self.access_token and self.access_token_expires_on > time.time():
return self.access_token
elif self.refresh_token and self.refresh_token_expires_on > time.time():
self.get_refresh_token()
else:
self.login()
return self.access_token
def get_auth_methods(self):
resp = requests.get(f"{self.baseurl}opencde/1.0/auth")
return resp.json()["supported_oauth2_flows"]
def get_versions(self):
resp = requests.get(f"{self.baseurl}opencde/versions")
resp_values = resp.json()["versions"]
for version in resp_values:
if "api_base_url" in version:
self.version_ids.update({version["version_id"]: version["api_base_url"]})
return self.version_ids
def set_version(self, version=None):
self.version = version
self.api_baseurl = self.version_ids[self.version]
def login(self):
resp = requests.get(f"{self.baseurl}opencde/1.0/auth")
values = resp.json()
self.auth_endpoint = values["oauth2_auth_url"]
self.token_endpoint = values["oauth2_token_url"]
with http.server.HTTPServer(("", 8080), OAuthReceiver) as server:
state = str(uuid.uuid4())
query = urllib.parse.urlencode(
{
"client_id": self.client_id,
"response_type": "code",
"state": state,
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_uri}",
}
)
if "?" in self.auth_endpoint:
webbrowser.open(f"{self.auth_endpoint}&{query}")
else:
webbrowser.open(f"{self.auth_endpoint}?{query}")
server.timeout = 100
server.state = state
server.handle_request()
if server.auth_code and server.auth_state == state:
data = {
"grant_type": "authorization_code",
"code": server.auth_code,
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_uri}",
}
auth_string = f"{self.client_id}:{self.client_secret}"
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
headers = {"Authorization": f"Basic {header_string}"}
self.set_tokens_from_response(requests.post(self.token_endpoint, data=data, headers=headers))
def get_refresh_token(self):
self.set_tokens_from_response(
requests.post(
self.token_endpoint,
params={
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
},
).json()
)
def get_new_access_token(self):
auth_string = f"{self.client_id}:{self.client_secret}"
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
headers = {"Authorization": f"Basic {header_string}"}
self.set_tokens_from_response(
requests.post(
self.token_endpoint,
data={
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
},
headers=headers,
).json()
)
def set_auth_method(self, method="authorization_code_grant"):
if method != "authorization_code_grant":
raise NotImplementedError(f"{method} not supported")
else:
self.auth_method = method
def set_tokens_from_response(self, response):
response = response.json()
self.access_token = response["access_token"]
self.refresh_token = response["refresh_token"]
self.access_token_expires_on = time.time() + response["expires_in"]
if "refresh_token_expires_in" in response:
self.refresh_token_expires_on = time.time() + response["refresh_token_expires_in"]
def get_projects(self) -> list:
return self.get(
f"/projects",
)
def get_project(
self,
project_id="",
) -> dict:
return self.get(
f"/projects/{project_id}",
{
"project_id": project_id,
},
)
def update_project(self, project_id="", data=None) -> dict:
url = f"{self.baseurl}/projects/{project_id}"
headers = {"Authorization": "Bearer " + self.get_access_token()}
resp = requests.put(url, headers=headers, data=data)
return resp.status_code, resp.text
def get_extensions(
self,
project_id="",
) -> dict:
return self.get(
f"/projects/{project_id}/extensions",
{
"project_id": project_id,
},
)
def get_topics(
self,
project_id="",
topics="",
query_string=None,
) -> list:
# return self.get(
# f"/projects/{project_id}/topics",
# {
# "project_id": project_id,
# "topics": topics,
# "query_string": query_string,
# },
# )
pass
def get_topic(self, project_id="", topic_id="") -> dict:
return self.get(
f"/projects/{project_id}/topics/{topic_id}",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def create_topic(self, project_id="", data=None):
return self.post(f"/projects/{project_id}/topics", data=data)
def update_topic(self, project_id="", topic_id="", data=None) -> dict:
return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data)
def delete_topic(self, project_id="", topic_id=""):
return self.delete(f"/projects/{project_id}/topics/{topic_id}")
def get_snippet(self, project_id="", topic_id="") -> str:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/snippet",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def update_snippet(self, project_id="", topic_id="", data=None):
return self.put(f"/projects/{project_id}/topics", data=data)
def get_files_information(self, project_id="") -> list:
return self.get(
f"/projects/{project_id}/files_information",
{
"project_id": project_id,
},
)
def get_files(self, project_id="", topic_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/files",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def update_files(
self,
project_id="",
topic_id="",
data=None,
params=None,
):
return self.put(
f"/projects/{project_id}/topics/{topic_id}/files",
data=data,
)
def get_comments(self, project_id="", topic_id="") -> list:
pass
def create_comments(
self,
project_id="",
topic_id="",
data=None,
params=None,
):
return self.post(
f"/projects/{project_id}/topics/{topic_id}/comments",
data=data,
)
def get_comment(self, project_id="", topic_id="", comment_id="") -> dict:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
{
"project_id": project_id,
"topic_id": topic_id,
"comment_id": comment_id,
},
)
def delete_comment(self, project_id="", topic_id="", comment_id=""):
return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}")
def update_comment(
self,
project_id="",
topic_id="",
comment_id="",
data=None,
):
return self.put(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
data=data,
)
def get_viewpoints(self, project_id="", topic_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def create_viewpoints(self, project_id="", topic_id="", data=None):
return self.post(
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
data=data,
)
def get_viewpoint(self, project_id="", topic_id="", viewpoint_id="") -> dict:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
{
"project_id": project_id,
"topic_id": topic_id,
"viewpoint_id": viewpoint_id,
},
)
def delete_viewpoint(
self,
project_id="",
topic_id="",
viewpoint_id="",
):
return self.delete(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
)
def get_snapshot(self, project_id="", topic_id="", viewpoint_id="") -> str:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot",
{
"project_id": project_id,
"topic_id": topic_id,
"viewpoint_id": viewpoint_id,
},
)
def get_bitmap(self, project_id="", topic_id="", viewpoint_id="", bitmap_id="") -> str:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}",
{
"project_id": project_id,
"topic_id": topic_id,
"viewpoint_id": viewpoint_id,
"bitmap_id": bitmap_id,
},
)
def get_selection(self, project_id="", topic_id="", viewpoint_id="") -> dict:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection",
{
"project_id": project_id,
"topic_id": topic_id,
"viewpoint_id": viewpoint_id,
},
)
def get_coloring(self, project_id="", topic_id="", viewpoint_id="") -> dict:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring",
{
"project_id": project_id,
"topic_id": topic_id,
"viewpoint_id": viewpoint_id,
},
)
def get_visibility(self, project_id="", topic_id="", viewpoint_id="") -> dict:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility",
{
"project_id": project_id,
"topic_id": topic_id,
"viewpoint_id": viewpoint_id,
},
)
def get_related_topics(self, project_id="", topic_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/related_topics",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def update_related_topics(
self,
project_id="",
topic_id="",
data=None,
):
return self.put(
f"/projects/{project_id}/topics/{topic_id}/related_topics",
data=data,
)
def get_document_references(self, project_id="", topic_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/document_references",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def create_document_reference(
self,
project_id="",
topic_id="",
data=None,
):
return self.post(
f"/projects/{project_id}/topics/{topic_id}/document_references",
data=data,
)
def update_document_references(
self,
project_id="",
topic_id="",
document_reference_id="",
data=None,
):
return self.put(
f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}",
data=data,
)
def get_documents(self, project_id="", topic_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/documents",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def create_document(
self,
project_id="",
topic_id="",
guid=None,
data=None,
):
headers = {
"Authorization": "Bearer " + self.get_access_token(),
"Content-type": "application/octet-stream",
}
response = requests.post(
f"/projects/{project_id}/topics/{topic_id}/documents",
data=data,
params={guid},
headers=headers,
)
def get_document(self, project_id="", topic_id="", document_id="") -> str:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/documents/{document_id}",
{
"project_id": project_id,
"topic_id": topic_id,
"document_id": document_id,
},
)
def get_topics_events(self, project_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/events",
{
"project_id": project_id,
},
)
def get_topic_events(self, project_id="", topic_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/events",
{
"project_id": project_id,
"topic_id": topic_id,
},
)
def get_comments_events(self, project_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/comments/events",
{
"project_id": project_id,
},
)
def get_comment_events(self, project_id="", topic_id="", comment_id="") -> list:
return self.get(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events",
{
"project_id": project_id,
"topic_id": topic_id,
"comment_id": comment_id,
},
)
+2
View File
@@ -44,6 +44,8 @@ endif
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
cp -r dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/
cp -r dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/entity_instance.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/file.py dist/blenderbim/libs/site/packages/ifcopenshell/
# Provides bcf functionality
cp -r dist/working/IfcOpenShell-0.6.0/src/bcf/bcf dist/blenderbim/libs/site/packages/
# Provides IFCClash functionality
@@ -68,6 +68,8 @@ class IfcExporter:
for ifc_definition_id, obj in IfcStore.id_map.items():
try:
if isinstance(obj, bpy.types.Material):
continue
self.sync_object_placement(obj)
self.sync_object_container(ifc_definition_id, obj)
except ReferenceError:
+34 -9
View File
@@ -159,8 +159,16 @@ class IfcImporter:
self.time = time.time()
print("{} :: {:.2f}".format(message, time.time() - self.time))
self.time = time.time()
self.update_progress(self.progress + 1)
def update_progress(self, progress):
if progress <= 100:
self.progress = progress
bpy.context.window_manager.progress_update(self.progress)
def execute(self):
bpy.context.window_manager.progress_begin(0, 100)
self.progress = 0
self.profile_code("Starting import process")
self.load_diff()
self.profile_code("Load diff")
@@ -200,7 +208,7 @@ class IfcImporter:
self.profile_code("Create native products")
self.create_products()
self.profile_code("Create products")
self.create_empty_products()
self.create_empty_and_2d_elements()
self.profile_code("Create empty products")
self.create_type_products()
self.profile_code("Create type products")
@@ -228,6 +236,8 @@ class IfcImporter:
self.profile_code("Mesh cleaning")
self.set_default_context()
self.profile_code("Setting default context")
self.update_progress(100)
bpy.context.window_manager.progress_end()
def is_element_far_away(self, element, is_meters=True):
try:
@@ -322,10 +332,10 @@ class IfcImporter:
return
project = self.file.by_type("IfcProject")[0]
site = self.find_decomposed_ifc_class(project, "IfcSite")
if site and self.is_element_far_away(site[0]):
if site and self.is_element_far_away(site[0], is_meters=False):
return self.guess_georeferencing(site[0])
building = self.find_decomposed_ifc_class(project, "IfcBuilding")
if building and self.is_element_far_away(building[0]):
if building and self.is_element_far_away(building[0], is_meters=False):
return self.guess_georeferencing(building[0])
return self.guess_absolute_coordinate()
@@ -546,12 +556,21 @@ class IfcImporter:
if not valid_file:
return False
checkpoint = time.time()
total = 0
total_created = 0
approx_total_products = len(self.include_elements) or len(self.file.by_type("IfcElement"))
start_progress = self.progress
progress_range = 85 - start_progress
while True:
total += 1
if total % 250 == 0:
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
if total_created % 250 == 0:
print(
"{} / ~{} elements processed in {:.2f}s ...".format(
total_created, approx_total_products, time.time() - checkpoint
)
)
checkpoint = time.time()
self.update_progress(
((total_created / approx_total_products) * progress_range) + start_progress
)
shape = iterator.get()
if shape:
product = self.file.by_id(shape.guid)
@@ -565,18 +584,24 @@ class IfcImporter:
pass
else:
self.create_product(product, shape)
total_created += 1
if not iterator.next():
break
print("Done creating geometry")
def create_empty_products(self):
for element in self.file.by_type("IfcProduct"):
def create_empty_and_2d_elements(self):
curve_products = []
for element in self.file.by_type("IfcElement"):
if element.id() in self.added_data:
continue
if element.is_a("IfcPort"):
continue
if not element.Representation:
self.create_product(element)
else:
curve_products.append(element)
if curve_products:
self.create_curve_products(curve_products)
def create_annotation(self):
self.create_curve_products(self.file.by_type("IfcAnnotation"))
@@ -117,19 +117,20 @@ class AddRepresentation(bpy.types.Operator):
if s.material and not s.material.BIMMaterialProperties.ifc_style_id
]
ifcopenshell.api.run(
"style.assign_representation_styles",
self.file,
**{
"shape_representation": result,
"styles": [
self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
for s in obj.material_slots
if s.material
],
"should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
},
)
if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons):
ifcopenshell.api.run(
"style.assign_representation_styles",
self.file,
**{
"shape_representation": result,
"styles": [
self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
for s in obj.material_slots
if s.material
],
"should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
},
)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, **{"product": product, "representation": result}
)
@@ -160,6 +161,7 @@ class SwitchRepresentation(bpy.types.Operator):
ifc_definition_id: bpy.props.IntProperty()
should_reload: bpy.props.BoolProperty()
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
def execute(self, context):
self.element_obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
@@ -171,11 +173,17 @@ class SwitchRepresentation(bpy.types.Operator):
mesh = bpy.data.meshes.get(self.mesh_name)
if mesh:
self.element_obj.data.user_remap(mesh)
self.switch_mesh(mesh)
if not mesh or self.should_reload:
self.pull_mesh_from_ifc()
return {"FINISHED"}
def switch_mesh(self, mesh):
if self.should_switch_all_meshes or self.file.by_id(self.oprops.ifc_definition_id).is_a("IfcTypeProduct"):
self.element_obj.data.user_remap(mesh)
else:
self.element_obj.data = mesh
def get_mesh_name(self):
representation = self.resolve_mapped_representation(self.file.by_id(self.ifc_definition_id))
return "{}/{}".format(self.context_of_items.id(), representation.id())
@@ -205,7 +213,7 @@ class SwitchRepresentation(bpy.types.Operator):
mesh = ifc_importer.create_mesh(element, shape)
mesh.name = self.mesh_name
mesh.BIMMeshProperties.ifc_definition_id = self.ifc_definition_id
self.element_obj.data.user_remap(mesh)
self.switch_mesh(mesh)
material_creator = import_ifc.MaterialCreator(ifc_import_settings, ifc_importer)
material_creator.load_existing_materials()
material_creator.create(element, self.element_obj, mesh)
@@ -330,19 +338,20 @@ class UpdateRepresentation(bpy.types.Operator):
if s.material and not s.material.BIMMaterialProperties.ifc_style_id
]
ifcopenshell.api.run(
"style.assign_representation_styles",
self.file,
**{
"shape_representation": new_representation,
"styles": [
self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
for s in obj.material_slots
if s.material
],
"should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
},
)
if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons):
ifcopenshell.api.run(
"style.assign_representation_styles",
self.file,
**{
"shape_representation": new_representation,
"styles": [
self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
for s in obj.material_slots
if s.material
],
"should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
},
)
# TODO: move this into a replace_representation usecase or something
for inverse in self.file.get_inverse(old_representation):
@@ -366,7 +375,9 @@ class UpdateParametricRepresentation(bpy.types.Operator):
props = obj.data.BIMMeshProperties
parameter = props.ifc_parameters[self.index]
element = IfcStore.get_file().by_id(parameter.step_id)[parameter.index] = parameter.value
bpy.ops.bim.switch_representation(ifc_definition_id=props.ifc_definition_id, should_reload=True)
bpy.ops.bim.switch_representation(
ifc_definition_id=props.ifc_definition_id, should_reload=True, should_switch_all_meshes=True
)
return {"FINISHED"}
@@ -41,6 +41,7 @@ class BIM_PT_representations(Panel):
row.label(text=representation["ContextOfItems"]["TargetView"])
row.label(text=representation["RepresentationType"])
op = row.operator("bim.switch_representation", icon="OUTLINER_DATA_MESH", text="")
op.should_switch_all_meshes = True
op.should_reload = True
op.ifc_definition_id = ifc_definition_id
op.disable_opening_subtractions = False
@@ -71,10 +72,12 @@ class BIM_PT_mesh(Panel):
row = layout.row(align=True)
op = row.operator("bim.switch_representation", text="Bake Voids", icon="SELECT_SUBTRACT")
op.should_switch_all_meshes=True
op.should_reload = True
op.ifc_definition_id = props.ifc_definition_id
op.disable_opening_subtractions = False
op = row.operator("bim.switch_representation", text="Dynamic Voids", icon="SELECT_INTERSECT")
op.should_switch_all_meshes=True
op.should_reload = True
op.ifc_definition_id = props.ifc_definition_id
op.disable_opening_subtractions = True
@@ -473,7 +473,6 @@ class EditAssignedMaterial(bpy.types.Operator):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
props = obj.BIMObjectMaterialProperties
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
material_set = self.file.by_id(self.material_set)
if product_data["type"] == "IfcMaterial":
bpy.ops.bim.unassign_material(obj=obj.name)
@@ -482,6 +481,8 @@ class EditAssignedMaterial(bpy.types.Operator):
bpy.ops.bim.disable_editing_assigned_material(obj=obj.name)
return {"FINISHED"}
material_set = self.file.by_id(self.material_set)
attributes = {}
for attribute in props.material_set_attributes:
attributes[attribute.name] = None if attribute.is_null else attribute.string_value
@@ -495,16 +496,21 @@ class EditAssignedMaterial(bpy.types.Operator):
if self.material_set_usage:
material_set_usage = self.file.by_id(self.material_set_usage)
attributes = blenderbim.bim.helper.export_attributes(props.material_set_usage_attributes)
if attributes.get("CardinalPoint", None):
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
ifcopenshell.api.run(
"material.edit_profile_usage",
self.file,
**{"usage": material_set_usage, "attributes": attributes},
)
if material_set_usage.is_a("IfcMaterialLayerSetUsage"):
ifcopenshell.api.run(
"material.edit_layer_usage",
self.file,
**{"usage": material_set_usage, "attributes": attributes},
)
Data.load_layer_usages()
elif material_set_usage.is_a("IfcMaterialProfileSetUsage"):
if attributes.get("CardinalPoint", None):
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
ifcopenshell.api.run(
"material.edit_profile_usage",
self.file,
**{"usage": material_set_usage, "attributes": attributes},
)
Data.load_profile_usages()
if material_set.is_a("IfcMaterialConstituentSet"):
@@ -97,6 +97,8 @@ class BIM_PT_object_material(Panel):
self.material_set_data = Data.lists[self.material_set_id]
self.set_items = self.material_set_data["Materials"] or []
self.set_item_name = "list_item"
else:
self.material_set_id = 0
return self.draw_material_ui()
row = self.layout.row(align=True)
@@ -316,11 +318,11 @@ class BIM_PT_object_material(Panel):
item_name = item.get("Name", "Unnamed") or "Unnamed"
thickness = item.get("LayerThickness")
if thickness:
item_name += f" ({thickness})"
item_name += f" ({thickness:.3f})"
total_thickness += thickness
row.label(text=item_name, icon="ALIGN_CENTER")
row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL")
if total_thickness:
row = self.layout.row(align=True)
row.label(text=f"Total Thickness: {total_thickness}")
row.label(text=f"Total Thickness: {total_thickness:.3f}")
@@ -3,7 +3,8 @@ from . import handler, prop, ui, grid, product, wall, slab, stair, door, window,
classes = (
product.AddTypeInstance,
wall.AddWall,
product.AlignProduct,
workspace.Hotkey,
wall.JoinWall,
wall.AlignWall,
wall.FlipWall,
@@ -1,7 +1,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api
from blenderbim.bim.module.model import product, wall, slab, column
from blenderbim.bim.module.model import product, wall, slab, profile
from blenderbim.bim.ifc import IfcStore
from bpy.app.handlers import persistent
@@ -51,17 +51,17 @@ def load_post(*args):
"type.assign_type", "BlenderBIM.DumbSlab.RegenerateFromType", slab.DumbSlabPlaner().regenerate_from_type
)
IfcStore.add_element_listener(column.element_listener)
IfcStore.add_element_listener(profile.element_listener)
ifcopenshell.api.add_pre_listener(
"geometry.add_representation", "BlenderBIM.DumbColumn.EnsureSolid", column.ensure_solid
"geometry.add_representation", "BlenderBIM.DumbProfile.EnsureSolid", profile.ensure_solid
)
ifcopenshell.api.add_post_listener(
"material.edit_profile",
"BlenderBIM.DumbColumn.RegenerateFromProfile",
column.DumbColumnRegenerator().regenerate_from_profile,
"BlenderBIM.DumbProfile.RegenerateFromProfile",
profile.DumbProfileRegenerator().regenerate_from_profile,
)
ifcopenshell.api.add_post_listener(
"type.assign_type",
"BlenderBIM.DumbColumn.RegenerateFromType",
column.DumbColumnRegenerator().regenerate_from_type,
"BlenderBIM.DumbProfile.RegenerateFromType",
profile.DumbProfileRegenerator().regenerate_from_type,
)
@@ -1,12 +1,13 @@
import bpy
import mathutils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
from . import wall, slab, column
from . import wall, slab, profile
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.pset.data import Data as PsetData
from mathutils import Vector
from mathutils import Vector, Matrix
class AddTypeInstance(bpy.types.Operator):
@@ -35,8 +36,9 @@ class AddTypeInstance(bpy.types.Operator):
obj = slab.DumbSlabGenerator(self.file.by_id(int(relating_type))).generate()
if obj:
return {"FINISHED"}
elif ifc_class == "IfcColumnType":
obj = column.DumbColumnGenerator(self.file.by_id(int(relating_type))).generate()
elif ifc_class in ["IfcColumnType", "IfcBeamType"]:
obj = profile.DumbProfileGenerator(self.file.by_id(int(relating_type))).generate()
return {"FINISHED"}
if obj:
return {"FINISHED"}
# A cube
@@ -73,6 +75,52 @@ class AddTypeInstance(bpy.types.Operator):
return {"FINISHED"}
class AlignProduct(bpy.types.Operator):
bl_idname = "bim.align_product"
bl_label = "Align Wall"
bl_options = {"REGISTER", "UNDO"}
align_type: bpy.props.StringProperty()
def execute(self, context):
selected_objs = context.selected_objects
if len(selected_objs) < 2 or not context.active_object:
return {"FINISHED"}
if self.align_type == "CENTERLINE":
point = context.active_object.matrix_world @ (
Vector(context.active_object.bound_box[0]) + (context.active_object.dimensions / 2)
)
elif self.align_type == "POSITIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[6])
elif self.align_type == "NEGATIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[0])
active_x_axis = context.active_object.matrix_world.to_quaternion() @ Vector((1, 0, 0))
active_y_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 1, 0))
active_z_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 0, 1))
x_distances = self.get_axis_distances(point, active_x_axis)
y_distances = self.get_axis_distances(point, active_y_axis)
if abs(sum(x_distances)) < abs(sum(y_distances)):
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_x_axis * -x_distances[i]) @ obj.matrix_world
else:
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_y_axis * -y_distances[i]) @ obj.matrix_world
return {"FINISHED"}
def get_axis_distances(self, point, axis):
results = []
for obj in bpy.context.selected_objects:
if self.align_type == "CENTERLINE":
obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
elif self.align_type == "POSITIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
elif self.align_type == "NEGATIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[0])
results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis))
return results
def generate_box(usecase_path, ifc_file, settings):
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
if not box_context:
@@ -98,6 +146,7 @@ def generate_box(usecase_path, ifc_file, settings):
**{"product": product, "representation": new_box}
)
def regenerate_profile_usage(usecase_path, ifc_file, settings):
elements = []
if ifc_file.schema == "IFC2X3":
@@ -117,4 +166,6 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
continue
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if representation:
bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True)
bpy.ops.bim.switch_representation(
obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True
)
@@ -31,7 +31,7 @@ def mode_callback(obj, data):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbColumn":
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return
IfcStore.edited_objs.add(obj)
bm = bmesh.from_edit_mesh(obj.data)
@@ -43,7 +43,7 @@ def mode_callback(obj, data):
def ensure_solid(usecase_path, ifc_file, settings):
product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbColumn":
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return
material = ifcopenshell.util.element.get_material(product)
if material and material.is_a("IfcMaterialProfileSetUsage"):
@@ -53,7 +53,7 @@ def ensure_solid(usecase_path, ifc_file, settings):
settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage"
class DumbColumnGenerator:
class DumbProfileGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
@@ -75,9 +75,9 @@ class DumbColumnGenerator:
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
return self.create_column()
return self.create_profile()
def create_column(self):
def create_profile(self):
# A cube
verts = [
Vector((-1, -1, -1)),
@@ -99,17 +99,23 @@ class DumbColumnGenerator:
[0, 2, 6, 4],
]
mesh = bpy.data.meshes.new(name="Dumb Column")
mesh = bpy.data.meshes.new(name="Dumb Profile")
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new("Column", mesh)
obj.name = "Column"
obj = bpy.data.objects.new("Profile", mesh)
obj.location = self.location
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = self.collection_obj.location[2]
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(
obj=obj.name, ifc_class="IfcColumn", predefined_type="COLUMN", should_add_representation=False
)
if self.relating_type.is_a("IfcColumnType"):
obj.name = "Column"
bpy.ops.bim.assign_class(
obj=obj.name, ifc_class="IfcColumn", predefined_type="COLUMN", should_add_representation=False
)
elif self.relating_type.is_a("IfcBeamType"):
obj.name = "Beam"
bpy.ops.bim.assign_class(
obj=obj.name, ifc_class="IfcBeam", predefined_type="BEAM", should_add_representation=False
)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name)
profile_set_usage = ifcopenshell.util.element.get_material(element)
@@ -120,15 +126,17 @@ class DumbColumnGenerator:
profile_set_usage=profile_set_usage.id(),
)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True)
bpy.ops.bim.switch_representation(
obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True
)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbColumn"})
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"})
MaterialData.load(self.file)
obj.select_set(True)
return obj
class DumbColumnRegenerator:
class DumbProfileRegenerator:
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = IfcStore.get_file()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
@@ -165,4 +173,6 @@ class DumbColumnRegenerator:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if representation:
bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True)
bpy.ops.bim.switch_representation(
obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True
)
@@ -36,21 +36,6 @@ def mode_callback(obj, data):
IfcStore.edited_objs.add(obj)
class AddWall(bpy.types.Operator):
bl_idname = "bim.add_wall"
bl_label = "Add Wall"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
props = context.scene.BIMModelProperties
bpy.ops.bim.add_type_instance(ifc_class="IfcWallType", relating_type=int(props.relating_type))
return {"FINISHED"}
class JoinWall(bpy.types.Operator):
bl_idname = "bim.join_wall"
bl_label = "Join Wall"
@@ -65,7 +50,13 @@ class JoinWall(bpy.types.Operator):
for obj in selected_objs:
DumbWallJoiner(obj, obj).unjoin()
return {"FINISHED"}
if len(selected_objs) < 2 or not context.active_object:
if not context.active_object:
return {"FINISHED"}
if len(selected_objs) == 1:
DumbWallJoiner(context.active_object, target_coordinate=context.scene.cursor.location).extend()
IfcStore.edited_objs.add(context.active_object)
return {"FINISHED"}
if len(selected_objs) < 2:
return {"FINISHED"}
for obj in selected_objs:
if obj == context.active_object:
@@ -327,16 +318,19 @@ class DumbWallJoiner:
# 2. Given an "end face", identify a side "target face" of the other wall
# to project towards.
# 3. Project the vertices of an "end face" to the "target face".
def __init__(self, wall1, wall2):
# Alternatively, a target coordinate may be provided as an imaginary point for the wall to join to
def __init__(self, wall1, wall2=None, target_coordinate=None):
self.wall1 = wall1
self.wall2 = wall2
self.target_coordinate = target_coordinate
self.should_project_to_frontface = True
self.should_attempt_v_junction_projection = False
self.initialise_convenience_variables()
def initialise_convenience_variables(self):
self.wall1_matrix = self.wall1.matrix_world
self.wall2_matrix = self.wall2.matrix_world
if self.wall2:
self.wall2_matrix = self.wall2.matrix_world
self.pos_x = self.wall1_matrix.to_quaternion() @ Vector((1, 0, 0))
self.neg_x = self.wall1_matrix.to_quaternion() @ Vector((-1, 0, 0))
@@ -354,6 +348,26 @@ class DumbWallJoiner:
self.wall1.data.vertices[v].co[0] = max_x
self.recalculate_origins()
# An extension is where a single end of wall1 is projected to an imaginary
# plane denoted by the target coordinate.
def extend(self):
wall1_min_faces, wall1_max_faces = self.get_wall_end_faces(self.wall1)
ef1_distance = abs(mathutils.geometry.distance_point_to_plane(
self.wall1_matrix @ self.wall1.data.vertices[wall1_min_faces[0].vertices[0]].co,
self.target_coordinate,
self.pos_x,
))
ef2_distance = abs(mathutils.geometry.distance_point_to_plane(
self.wall1_matrix @ self.wall1.data.vertices[wall1_max_faces[0].vertices[0]].co,
self.target_coordinate,
self.neg_x,
))
if ef1_distance < ef2_distance:
self.project_end_faces_to_target(wall1_min_faces)
else:
self.project_end_faces_to_target(wall1_max_faces)
self.recalculate_origins()
# A T-junction is an ordered operation where a single end of wall1 is joined
# to wall2 if possible (i.e. walls aren't parallel). Wall2 is not modified.
# First, wall1 end faces are identified. We attempt to project an end face
@@ -439,7 +453,8 @@ class DumbWallJoiner:
def recalculate_origins(self):
bpy.context.view_layer.update()
recalculate_dumb_wall_origin(self.wall1)
recalculate_dumb_wall_origin(self.wall2)
if self.wall2:
recalculate_dumb_wall_origin(self.wall2)
def swap_walls(self):
self.wall1, self.wall2 = self.wall2, self.wall1
@@ -467,6 +482,14 @@ class DumbWallJoiner:
local_point = wall_matrix.inverted() @ point
wall.data.vertices[v].co = local_point
def project_end_faces_to_target(self, end_faces):
for end_face in end_faces:
for v in end_face.vertices:
vertex = self.wall1_matrix @ self.wall1.data.vertices[v].co
self.wall1.data.vertices[v].co = self.wall1_matrix.inverted() @ mathutils.geometry.intersect_line_plane(
vertex, vertex + self.pos_x, self.target_coordinate, self.pos_x
)
# A projection target face is a side face on the target wall that has a
# significant local Y component to its normal (i.e. is not pointing up or
# down or something). In addition, its plane must intersect with the
@@ -1,6 +1,7 @@
import os
import bpy
from bpy.types import WorkSpaceTool
from blenderbim.bim.ifc import IfcStore
class BimTool(WorkSpaceTool):
@@ -16,44 +17,73 @@ class BimTool(WorkSpaceTool):
bl_keymap = (
# ("bim.wall_tool_op", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
("bim.add_wall", {"type": "A", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.join_wall", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("join_type", "T")]}),
("bim.add_type_instance", {"type": "A", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "E")]}),
("bim.join_wall", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("join_type", "L")]}),
("bim.join_wall", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("join_type", "V")]}),
("bim.flip_wall", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.split_wall", {"type": "S", "value": "PRESS", "shift": True}, {"properties": []}),
(
"bim.align_wall",
{"type": "X", "value": "PRESS", "shift": True},
{"properties": [("align_type", "EXTERIOR")]},
),
(
"bim.align_wall",
{"type": "C", "value": "PRESS", "shift": True},
{"properties": [("align_type", "CENTERLINE")]},
),
(
"bim.align_wall",
{"type": "V", "value": "PRESS", "shift": True},
{"properties": [("align_type", "INTERIOR")]},
),
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "X")]}),
("bim.hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "C")]}),
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "V")]}),
)
def draw_settings(context, layout, tool):
props = context.scene.BIMModelProperties
row = layout.row(align=True)
props = context.scene.BIMTypeProperties
row.prop(props, "ifc_class", text="")
row.prop(props, "relating_type", text="")
row.label(text="", icon="BLANK1")
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Add", icon="EVENT_A")
row.label(text="Extend", icon="EVENT_E")
row.label(text="Butt", icon="EVENT_T")
row.label(text="Mitre", icon="EVENT_Y")
row.label(text="Flip", icon="EVENT_F")
row.label(text="Split", icon="EVENT_S")
if props.ifc_class == "IfcWallType":
row.label(text="Extend", icon="EVENT_E")
row.label(text="Butt", icon="EVENT_T")
row.label(text="Mitre", icon="EVENT_Y")
row.label(text="Flip", icon="EVENT_F")
row.label(text="Split", icon="EVENT_S")
row.label(text="", icon="EVENT_X")
row.label(text="", icon="EVENT_C")
row.label(text="", icon="EVENT_V")
row.label(text="Align")
class Hotkey(bpy.types.Operator):
bl_idname = "bim.hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.props = context.scene.BIMTypeProperties
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def hotkey_C(self):
if self.props.ifc_class == "IfcWallType":
bpy.ops.bim.align_wall(align_type="CENTERLINE")
else:
bpy.ops.bim.align_product(align_type="CENTERLINE")
def hotkey_E(self):
if self.props.ifc_class == "IfcWallType":
bpy.ops.bim.join_wall(join_type="T")
def hotkey_V(self):
if self.props.ifc_class == "IfcWallType":
bpy.ops.bim.align_wall(align_type="INTERIOR")
else:
bpy.ops.bim.align_product(align_type="POSITIVE")
def hotkey_X(self):
if self.props.ifc_class == "IfcWallType":
bpy.ops.bim.align_wall(align_type="EXTERIOR")
else:
bpy.ops.bim.align_product(align_type="NEGATIVE")
@@ -265,13 +265,29 @@ class CopyClass(bpy.types.Operator):
for obj in objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
result = ifcopenshell.api.run(
"root.copy_class", self.file, **{"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)}
)
old_element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
result = ifcopenshell.api.run("root.copy_class", self.file, **{"product": old_element})
IfcStore.link_element(result, obj)
relating_type = ifcopenshell.util.element.get_type(result)
if relating_type and relating_type.RepresentationMaps:
bpy.ops.bim.assign_type(relating_type=relating_type.id(), related_object=obj.name)
else:
bpy.ops.bim.add_representation(obj=obj.name)
if result.is_a("IfcSpatialElement") or element.is_a("IfcSpatialStructureElement"):
self.place_in_spatial_collection(old_element, obj)
return {"FINISHED"}
def place_in_spatial_collection(self, old_element, obj):
aggregate = ifcopenshell.util.element.get_aggregate(old_element)
if not aggregate:
return
container_obj = IfcStore.get_element(aggregate.id())
for collection in obj.users_collection:
collection.objects.unlink(obj)
if "Ifc" in collection.name:
parent_collection = collection
for collection in container_obj.users_collection:
if collection.name == container_obj.name:
new = bpy.data.collections.new(obj.name)
new.objects.link(obj)
collection.children.link(new)
@@ -22,7 +22,9 @@ class AssignType(bpy.types.Operator):
self.file = IfcStore.get_file()
relating_type = self.relating_type or int(context.active_object.BIMTypeProperties.relating_type)
related_objects = (
[bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects
[bpy.data.objects.get(self.related_object)]
if self.related_object
else bpy.context.selected_objects or [bpy.context.active_object]
)
for related_object in related_objects:
oprops = related_object.BIMObjectProperties
@@ -39,15 +41,19 @@ class AssignType(bpy.types.Operator):
MaterialData.load(IfcStore.get_file(), oprops.ifc_definition_id)
representation_ids = GeometryData.products[oprops.ifc_definition_id]
if not representation_ids:
pass # TODO: clear geometry? Make void? Make none type?
pass # TODO: clear geometry? Make void? Make none type?
has_switched = False
for representation_id in representation_ids:
representation = GeometryData.representations[representation_id]
if representation["ContextOfItems"]["ContextIdentifier"] == "Body":
bpy.ops.bim.switch_representation(obj=related_object.name, ifc_definition_id=representation_id)
bpy.ops.bim.switch_representation(
obj=related_object.name, ifc_definition_id=representation_id, should_switch_all_meshes=False
)
has_switched = True
if not has_switched and representation_ids:
bpy.ops.bim.switch_representation(obj=related_object.name, ifc_definition_id=representation_id)
bpy.ops.bim.switch_representation(
obj=related_object.name, ifc_definition_id=representation_id, should_switch_all_meshes=False
)
bpy.ops.bim.disable_editing_type(obj=related_object.name)
MaterialData.load(self.file)
+1 -1
View File
@@ -745,7 +745,7 @@ int main(int argc, char** argv) {
std::uniform_int_distribution<int> index_dist('A', 'Z');
{
std::string v = ".ifcopenshell.";
output_temp_filename += path_t(v.begin(), v.end());
output_temp_filename = path_t(v.begin(), v.end());
}
for (int i = 0; i < 8; ++i) {
output_temp_filename.push_back(static_cast<path_t::value_type>(index_dist(rng)));
@@ -97,3 +97,53 @@ def remove_post_listener(usecase_path, name, callback):
def remove_all_listeners():
pre_listeners.clear()
post_listeners.clear()
def extract_docs(module, usecase):
import typing
import inspect
import collections
results = []
inputs = collections.OrderedDict()
function_init = getattr(getattr(ifcopenshell.api, module), usecase).Usecase.__init__
function_execute = getattr(getattr(ifcopenshell.api, module), usecase).Usecase.execute
node_data = {"module": module, "usecase": usecase}
signature = inspect.signature(function_init)
for name, parameter in signature.parameters.items():
if name == "self":
continue
inputs[name] = {"name": name}
if not isinstance(parameter.default, object):
inputs[name]["default"] = parameter.default
type_hints = typing.get_type_hints(function_init)
for name, socket_data in inputs.items():
type_hint = type_hints[name]
if isinstance(type_hint, typing._UnionGenericAlias):
inputs[name]["type"] = [t.__name__ for t in typing.get_args(type_hint)]
else:
inputs[name]["type"] = type_hint.__name__
description = ""
for i, line in enumerate(function_init.__doc__.split("\n")):
line = line.strip()
if i == 0:
node_data["name"] = line
elif line.startswith(":return:"):
node_data["output"] = {"name": line.split(":")[2].strip(), "description": line.split(":")[3].strip()}
elif line.startswith(":param"):
param_name = line.split(":")[1].strip().replace("param ", "")
inputs[param_name]["description"] = line.split(":")[2].strip()
elif i >= 2:
description += line
if "output" in node_data:
node_data["output"]["type"] = typing.get_type_hints(function_execute)["return"].__name__
node_data["description"] = description.strip()
node_data["inputs"] = inputs
return node_data
@@ -22,8 +22,6 @@ class Usecase:
"unit_scale": None, # A scale factor to apply for all vectors in case the unit is different
"should_force_faceted_brep": False, # If we should force faceted breps for meshes
"should_force_triangulation": False, # If we should force triangulation for meshes
"is_wireframe": False, # If the geometry is a wireframe
"is_curve": False, # If the geometry is a Blender curve
"is_point_cloud": False, # If the geometry is a point cloud
# Possible IFC representation classes:
# IfcExtrudedAreaSolid/IfcRectangleProfileDef
@@ -204,9 +202,9 @@ class Usecase:
)
def create_variable_representation(self):
if self.settings["is_wireframe"]:
return self.create_wireframe_representation()
elif self.settings["is_curve"]:
if isinstance(self.settings["geometry"], bpy.types.Curve):
return self.create_curve3d_representation()
elif not len(self.settings["geometry"].polygons):
return self.create_curve3d_representation()
elif self.settings["is_point_cloud"]:
return self.create_point_cloud_representation()
@@ -0,0 +1,10 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
self.settings = {"usage": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["usage"], name, value)
@@ -3,12 +3,17 @@ import ifcopenshell
class Usecase:
def __init__(self, **settings):
self.settings = {"version": "IFC4"}
for key, value in settings.items():
self.settings[key] = value
def __init__(self, foobar: ifcopenshell.entity_instance, version: str = "IFC4", foo: str = None, bar: int = 1, baz: float = 0.5):
"""Create File
def execute(self):
Create a new IFC file object
:param version: The schema version of the IFC file. Choose from "IFC2X3" or "IFC4".
:return: file: The created IFC file object.
"""
self.settings = {"version": version}
def execute(self) -> ifcopenshell.file:
self.file = ifcopenshell.file(schema=self.settings["version"])
# TODO: add all metadata, pending bug #747
self.file.wrapped_data.header.file_name.name = "/dev/null" # Hehehe
@@ -22,5 +27,5 @@ class Usecase:
self.file.wrapped_data.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.wrapped_data.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.wrapped_data.header.file_name.authorization = "Nobody"
self.file.wrapped_data.header.file_description.description = ('ViewDefinition[DesignTransferView]',)
self.file.wrapped_data.header.file_description.description = ("ViewDefinition[DesignTransferView]",)
return self.file
@@ -48,7 +48,7 @@ class entity_instance(object):
>>> #423=IfcProductDefinitionShape($,$,(#409,#421))
"""
def __init__(self, e, file):
def __init__(self, e, file=None):
if isinstance(e, tuple):
e = ifcopenshell_wrapper.new_IfcBaseClass(*e)
super(entity_instance, self).__setattr__("wrapped_data", e)
@@ -126,7 +126,7 @@ class entity_instance(object):
return entity_instance.wrap_value(self.wrapped_data.get_argument(key), self.wrapped_data.file)
def __setitem__(self, idx, value):
if self.wrapped_data.file.transaction:
if self.wrapped_data.file and self.wrapped_data.file.transaction:
self.wrapped_data.file.transaction.store_edit(self, idx, value)
attr_type = real_attr_type = self.attribute_type(idx).title().replace(" ", "")
+27 -2
View File
@@ -264,19 +264,44 @@ class file(object):
eid = kwargs.pop("id", -1)
except:
pass
e = entity_instance((self.schema, type), self)
self.wrapped_data.add(e.wrapped_data, eid)
e.wrapped_data.this.disown()
# Create pairs of {attribute index, attribute value}.
# Keyword arguments are mapped to their corresponding
# numeric index with get_argument_index().
# @todo we should probably check that values for
# attributes are not passed as duplicates using
# both regular arguments and keyword arguments.
attrs = list(enumerate(args)) + [(e.wrapped_data.get_argument_index(name), arg) for name, arg in kwargs.items()]
# Don't store these attributes as transactions
# as the creation it self is already stored with
# it's arguments
if attrs:
transaction = self.transaction
self.transaction = None
for idx, arg in attrs:
e[idx] = arg
# Restore transaction status
if attrs:
self.transaction = transaction
# Once the values are populated add the instance
# to the file.
self.wrapped_data.add(e.wrapped_data, eid)
# The file container now handles the lifetime of
# this instance. Tell SWIG that it is no longer
# the owner.
e.wrapped_data.this.disown()
if self.transaction:
self.transaction.store_create(e)
return e
def __getattr__(self, attr):
+4
View File
@@ -1792,6 +1792,10 @@ IfcUtil::IfcBaseClass* IfcFile::addEntity(IfcUtil::IfcBaseClass* entity, int id)
build_inverses_(new_entity);
}
// @todo the id isn't actually used here, but instead
// clears the entire inverse cache map.
mark_entity_as_modified(0);
return new_entity;
}
@@ -0,0 +1,31 @@
import ifcopenshell
import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
curve_map = {}
for curve in self.file.by_type("IfcIndexedPolyCurve"):
if "IfcArcIndex" in [s.is_a() for s in curve.Segments]:
print("Could not convert curve due to arcs", curve)
continue
coordinates = curve.Points.CoordList
points = []
for i, segment in enumerate(curve.Segments):
segment = segment.wrappedValue
if i == 0:
points.append(self.file.createIfcCartesianPoint(coordinates[segment[0] - 1]))
points.append(self.file.createIfcCartesianPoint(coordinates[segment[1] - 1]))
polyline = self.file.create_entity("IfcPolyline", points)
curve_map[curve] = polyline
for curve, polyline in curve_map.items():
for inverse in self.file.get_inverse(curve):
ifcopenshell.util.element.replace_attribute(inverse, curve, polyline)
+1
View File
@@ -43,6 +43,7 @@ def nodes_index():
("ifc.get_property", "SvIfcGetProperty"),
("ifc.get_attribute", "SvIfcGetAttribute"),
("ifc.select_blender_objects", "SvIfcSelectBlenderObjects"),
("ifc.api", "SvIfcApi"),
],
)
]
+65
View File
@@ -0,0 +1,65 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcsverchok.helper
from bpy.props import StringProperty, EnumProperty
from sverchok.node_tree import SverchCustomTreeNode
from sverchok.data_structure import updateNode
#from blenderbim.bim.module.root.prop import getIfcClasses, getIfcProducts, refreshClasses, refreshPredefinedTypes
class SvIfcApi(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore):
bl_idname = "SvIfcApi"
bl_label = "IFC API"
usecase: StringProperty(name="Usecase", update=updateNode)
#schema: StringProperty(name="schema", update=updateNode, default="IFC4")
#ifc_product: EnumProperty(items=getIfcProducts, name="Products", update=refreshClasses)
#ifc_class: EnumProperty(items=getIfcClasses, name="Class", update=refreshPredefinedTypes)
#custom_ifc_class: StringProperty(name="Custom Ifc Class", update=updateNode)
def sv_init(self, context):
self.inputs.new("SvStringsSocket", "usecase").prop_name = "usecase"
#self.inputs.new("SvStringsSocket", "schema").prop_name = "schema"
#self.inputs.new("SvStringsSocket", "ifc_product").prop_name = "ifc_product"
#self.inputs.new("SvStringsSocket", "ifc_class").prop_name = "ifc_class"
#self.inputs.new("SvStringsSocket", "custom_ifc_class").prop_name = "custom_ifc_class"
self.outputs.new("SvVerticesSocket", "file")
def process(self):
print('process')
#self.sv_input_names = ["file", "ifc_product", "ifc_class", "custom_ifc_class"]
usecase = self.inputs["usecase"].sv_get()[0][0]
if usecase:
self.generate_node(*usecase.split("."))
self.sv_input_names = ["usecase"]
super().process()
def generate_node(self, module, usecase):
try:
node_data = ifcopenshell.api.extract_docs(module, usecase)
except:
print("Node not yet implemented:", module, usecase)
return
while len(self.inputs) > 1:
self.inputs.remove(self.inputs[-1])
for name, data in node_data["inputs"].items():
setattr(SvIfcApi, name, StringProperty(name=name))
self.inputs.new("SvStringsSocket", name).prop_name = name
#def process_ifc(self, file, ifc_product, ifc_class, custom_ifc_class):
def process_ifc(self, usecase):
print('run')
#self.outputs["file"].sv_set([ifcopenshell.api.run("project.create_file", version=schema)])
#if custom_ifc_class:
# self.outputs["entity"].sv_set([file.by_type(custom_ifc_class)])
#else:
# self.outputs["entity"].sv_set([file.by_type(ifc_class)])
def register():
bpy.utils.register_class(SvIfcApi)
def unregister():
bpy.utils.unregister_class(SvIfcApi)