Merge branch 'v0.8.0' of https://github.com/csritter/IfcOpenShell into v0.8.0

This commit is contained in:
Cristian Ritter
2024-08-26 15:34:55 -03:00
502 changed files with 124330 additions and 145664 deletions
-3
View File
@@ -73,9 +73,6 @@ src/ifcopenshell-python/test/build
# mypy cache
.mypy_cache
# bonsai libs
src/bonsai/bonsai/libs
# bonsai i18n
src/bonsai/bonsai/translations.py
+1 -1
View File
@@ -81,7 +81,7 @@ ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.7.1"
OCCT_VERSION = "7.5.3"
BOOST_VERSION = "1.80.0"
PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.9.11"
+10
View File
@@ -0,0 +1,10 @@
import bcf.v2.model
import bcf.v3.model
from typing import Union
BimSnippet = Union[bcf.v2.model.BimSnippet, bcf.v3.model.BimSnippet]
BitMap = Union[bcf.v2.model.VisualizationInfoBitmap, bcf.v3.model.Bitmap]
DocumentReference = Union[bcf.v2.model.TopicDocumentReference, bcf.v3.model.DocumentReference]
HeaderFile = Union[bcf.v2.model.HeaderFile, bcf.v3.model.File]
Topic = Union[bcf.v2.model.Topic, bcf.v3.model.Topic]
ViewPoint = Union[bcf.v2.model.ViewPoint, bcf.v3.model.ViewPoint]
+97 -1
View File
@@ -1,5 +1,101 @@
import tempfile
import bcf.v2.bcfxml
import bcf.v2.model
import bcf.v2.topic
import bcf.v3.bcfxml
import bcf.v3.model
import bcf.v3.topic
from typing import Union
import bcf.agnostic.model as mdl
from pathlib import Path
from typing import Union, Optional
from typing_extensions import assert_never
TopicHandler = Union[bcf.v2.topic.TopicHandler, bcf.v3.topic.TopicHandler]
def extract_file(
topic: TopicHandler,
entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.DocumentReference, mdl.BitMap],
bcfxml: Optional[Union[bcf.v2.bcfxml.BcfXml, bcf.v3.bcfxml.BcfXml]] = None,
outfile: Optional[Path] = None,
) -> Union[Path, str, None]:
"""Extracts an element with a file into a temporary directory
These include header files, bim snippets, document references, and
viewpoint bitmaps. External reference are not downloaded. Instead, the
URI reference is returned.
:param entity: The entity with a file reference to extract
:param outfile: If provided, save the header file to that location.
Otherwise, a temporary directory is created and the filename is
derived from the header's original filename.
:param bcfxml: The BCF XML file to use for resolving document references files.
Required only for BCF v3 document references (in BCF v3 internal documents
are stored at BCF root, not in the topic).
:return: The filepath of the extracted file. It may be a URL if the
header file is external.
"""
if isinstance(entity, mdl.DocumentReference):
if isinstance(entity, bcf.v2.model.TopicDocumentReference):
reference = entity.referenced_document
else:
reference = entity.document_guid
else:
reference = entity.reference
if not reference:
return None
# For v3 document references external documents are detected by empty document_guid.
# External bitmaps are not supported by bcf.
if not isinstance(entity, (bcf.v3.model.DocumentReference, mdl.BitMap)) and entity.is_external:
return reference
if isinstance(entity, bcf.v3.model.DocumentReference):
# Extract document reference filename and contents.
if not bcfxml:
raise TypeError("bcfxml is required for BCF v3 document references.")
assert isinstance(bcfxml, bcf.v3.bcfxml.BcfXml)
error_msg = f"BCF XML is missing document with guid '{reference}'."
if not bcfxml.documents:
raise Exception(error_msg)
definition_docs = bcfxml.documents.definition.documents
if not definition_docs:
raise Exception(error_msg)
docs = next((doc for doc in definition_docs.document if doc.guid == reference), None)
if not docs:
raise Exception(error_msg)
filename = docs.filename
bytes_data = bcfxml.documents.documents[filename]
else:
if isinstance(entity, mdl.BimSnippet):
bytes_data = topic.bim_snippet
assert isinstance(bytes_data, bytes)
elif isinstance(entity, mdl.HeaderFile):
bytes_data = topic.reference_files[reference]
elif isinstance(entity, mdl.BitMap):
bytes_data = next(
byte_data
for vp in topic.viewpoints.values()
for data_reference, byte_data in vp.bitmaps.items()
if data_reference == reference
)
elif isinstance(entity, bcf.v2.model.TopicDocumentReference):
assert isinstance(topic, bcf.v2.topic.TopicHandler)
bytes_data = topic.document_references[reference]
else:
assert_never(entity)
# We don't really need it if 'outfile' is None, just keeping type checker happy.
if isinstance(entity, mdl.HeaderFile) and entity.filename:
filename = entity.filename
else:
filename = Path(reference).name
if not outfile:
outfile = Path(tempfile.mkdtemp()) / filename
with open(outfile, "wb") as f:
f.write(bytes_data)
return outfile
+10 -48
View File
@@ -63,11 +63,21 @@ class TopicHandler:
"""Return the header of the topic."""
return self.markup.header if self.markup else None
@header.setter
def header(self, header: mdl.Header) -> None:
"""Set the header of the topic."""
self.markup.header = header
@property
def comments(self) -> list[mdl.Comment]:
"""Return the comments of the topic."""
return self.markup.comment if self.markup else []
@comments.setter
def comments(self, comments: list[mdl.Comment]) -> None:
assert self.markup
self.markup.comment = comments
@property
def bim_snippet(self) -> Optional[bytes]:
if not self._bim_snippet and self._topic_dir:
@@ -230,54 +240,6 @@ class TopicHandler:
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document])
def extract_file(
self, entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.TopicDocumentReference], outfile: Optional[Path] = None
) -> Union[Path, str, None]:
"""Extracts an element with a file into a temporary directory
These include header files, bim snippets, document references, and
viewpoint bitmaps. External reference are not downloaded. Instead, the
URI reference is returned.
:param entity: The entity with a file reference to extract
:param outfile: If provided, save the header file to that location.
Otherwise, a temporary directory is created and the filename is
derived from the header's original filename.
:return: The filepath of the extracted file. It may be a URL if the
header file is external.
:rtype: Path
"""
if hasattr(entity, "reference"):
reference = entity.reference
else:
reference = entity.referenced_document
if not reference:
return
if getattr(entity, "is_external", False):
return entity.reference
resolved_reference = self._topic_dir
for part in Path(reference).parts:
if part == "..":
resolved_reference = resolved_reference.parent
else:
resolved_reference = resolved_reference.joinpath(part)
if not outfile:
if getattr(entity, "filename", None):
filename = entity.filename
else:
filename = resolved_reference.name
outfile = Path(tempfile.mkdtemp()) / filename
with open(outfile, "wb") as f:
f.write(resolved_reference.read_bytes())
return outfile
def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler:
"""Add a viewpoint pointed at the placement of an IFC element to the topic.
+52 -2
View File
@@ -27,6 +27,7 @@ class TopicHandler:
) -> None:
self._markup: Optional[mdl.Markup] = None
self._viewpoints: Optional[dict[str, VisualizationInfoHandler]] = None
self._reference_files: Optional[dict[str, bytes]] = None
self._bim_snippet: Optional[bytes] = None
self._xml_handler = xml_handler or XmlParserSerializer()
self._topic_dir = topic_dir
@@ -49,22 +50,36 @@ class TopicHandler:
return self.markup.topic
@property
def guid(self) -> Optional[str]:
def guid(self) -> str:
"""Return the GUID of the topic."""
if self._markup:
return self.topic.guid
return self._topic_dir.name if self._topic_dir else None
return self._topic_dir.name if self._topic_dir else ""
@property
def header(self) -> Optional[mdl.Header]:
"""Return the header of the topic."""
return self.markup.header
@header.setter
def header(self, header: mdl.Header) -> None:
"""Set the header of the topic."""
self.markup.header = header
@property
def comments(self) -> list[mdl.Comment]:
"""Return the comments of the topic."""
return self.topic.comments.comment if self.topic.comments else []
@comments.setter
def comments(self, comments: list[mdl.Comment]) -> None:
topic_comments = self.topic.comments
if topic_comments is None:
if not comments:
return
self.topic.comments = (topic_comments := mdl.TopicComments())
topic_comments.comment = comments
@property
def bim_snippet(self) -> Optional[bytes]:
if not self._bim_snippet and self._topic_dir:
@@ -94,6 +109,27 @@ class TopicHandler:
return bim_snippet_path.read_bytes()
return None
@property
def reference_files(self) -> dict[str, bytes]:
if self._reference_files is not None:
return self._reference_files
self._reference_files = {}
if not self.header:
return self._reference_files
if not self.header.files:
return self._reference_files
for ref in self.header.files.file:
if ref.is_external:
continue
real_path = self._topic_dir
for path_part in ref.reference.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
self._reference_files[ref.reference] = real_path.read_bytes()
return self._reference_files
@classmethod
def create_new(
cls,
@@ -145,6 +181,7 @@ class TopicHandler:
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
self._save_reference_files(destination_zip)
def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None:
if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point):
@@ -165,6 +202,19 @@ class TopicHandler:
if self.bim_snippet:
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
def _save_reference_files(self, destination_zip: ZipFileInterface) -> None:
if not self.header:
return
if not self.header.files:
return
for ref in self.header.files.file:
if ref.is_external or not ref.reference:
continue
real_path = self._topic_dir
for path_part in ref.reference.split("/"):
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
destination_zip.writestr(real_path.at, self.reference_files[ref.reference])
def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler:
"""
Add a viewpoint tergeting an IFC element to the topic.
+2
View File
@@ -130,6 +130,8 @@ endif
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel odfpy --wheel-dir=./wheels
# Required by IFCCityJSON
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download cjio --dest=./wheels
# Required in general for sorting all sorts of stuff in a nice way
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download natsort --dest=./wheels
# Provides express rule validation for ifcopenshell.validate
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pytest --dest=./wheels
# Provides Brickschema functionality
+5
View File
@@ -59,6 +59,7 @@ last_error = None
last_actions: deque = deque(maxlen=10)
FIRST_INSTALLED_BBIM_VERSION: Union[str, None] = None
REINSTALLED_BBIM_VERSION: Union[str, None] = None
REGISTERED_BBIM_PACKAGE: str
def initialize_bbim_semver():
@@ -238,6 +239,10 @@ if IN_BLENDER:
if platform.system() == "Windows":
clean_up_dlls_safe_links()
import bonsai
bonsai.REGISTERED_BBIM_PACKAGE = __package__
import bonsai.bim
current_version = bbim_semver["version"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+35 -21
View File
@@ -20,14 +20,6 @@ sio = socketio.AsyncServer(
cors_allowed_origins="*",
async_mode="aiohttp",
)
# sio.instrument(
# auth={
# "username": "admin",
# "password": "admin",
# }
# )
app = web.Application()
sio.attach(app)
@@ -37,6 +29,8 @@ blender_messages = {}
blender_theme = {}
# we define two namespaces, one for Bonsai and one for Web UI
# each namespace has its own event handlers that are called when an event is emitted by a client
# Web namespace
class WebNamespace(socketio.AsyncNamespace):
def __init__(self, namespace):
@@ -74,7 +68,8 @@ class WebNamespace(socketio.AsyncNamespace):
await self.emit("csv_data", {"blenderId": blenderId, "data": messages["csv_data"]}, room=sid)
if "gantt_data" in messages:
await self.emit("gantt_data", {"blenderId": blenderId, "data": messages["gantt_data"]}, room=sid)
if "demo_data" in messages:
await self.emit("demo_data", {"blenderId": blenderId, "data": messages["demo_data"]}, room=sid)
# Blender namespace
class BlenderNamespace(socketio.AsyncNamespace):
@@ -117,38 +112,47 @@ class BlenderNamespace(socketio.AsyncNamespace):
async def on_theme_data(self, sid, data):
global blender_theme
print(f"Blender theme data")
blender_theme = data
await sio.emit("theme_data", data, namespace="/web")
# Attach namespaces
sio.register_namespace(WebNamespace("/web"))
sio.register_namespace(BlenderNamespace("/blender"))
# this function will be called when the event demo_data is emitted
async def on_demo_data(self, sid, data):
print(f"Demo data from Blender client {sid}")
blender_messages[sid]["demo_data"] = data
await sio.emit("demo_data", {"blenderId": sid, "data": data}, namespace="/web")
# Define a route to render the index.html template
async def index(request):
async def schedules(request):
with open("templates/index.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def gantt(request):
async def sequencing(request):
with open("templates/gantt.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def drawings(request):
async def documentation(request):
with open("templates/drawings.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
# This is a request handler for the /demo URL endpoint.
# It serves the demo HTML file after using pystache to render the variables
async def demo(request):
with open("templates/demo.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def on_startup(app):
pid_file = "running_pid.json"
@@ -164,12 +168,22 @@ async def on_startup(app):
json.dump(pids, f, indent=4)
app.router.add_get("/", index)
app.router.add_get("/drawings", drawings)
app.router.add_get("/gantt", gantt)
# Add on startup function
app.on_startup.append(on_startup)
# Attach namespaces
sio.register_namespace(WebNamespace("/web"))
sio.register_namespace(BlenderNamespace("/blender"))
# Regsier routes
app.router.add_get("/", schedules)
app.router.add_get("/documentation", documentation)
app.router.add_get("/sequencing", sequencing)
app.router.add_get("/demo", demo)
# Add static files
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
app.router.add_static("/static/", path="./static", name="static")
app.on_startup.append(on_startup)
def main():
@@ -0,0 +1,348 @@
: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: 1.5rem;
--nav-height: 0.625rem;
--border-radius: 0.3rem;
}
: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);
}
body {
background-color: var(--blender-window-background, var(--bg-color));
color: var(--blender-text, 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);
}
h3 {
margin-top: 0;
}
nav {
background-color: var(--blender-top-bar-header, 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(--blender-tab-outline, var(--nav-border-color));
}
nav .logo {
margin-left: var(--margin-large);
height: var(--logo-height);
}
nav ul {
list-style-type: none;
margin: 0;
display: flex;
flex: 1;
justify-content: center;
}
nav ul li {
margin-right: var(--margin-large);
}
nav ul li a {
text-decoration: none;
color: var(--blender-text, var(--nav-link-color));
}
nav ul li a:hover,
nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
#toggle-theme {
border: none;
background: none;
cursor: pointer;
font-size: var(--font-size-large);
margin-right: var(--margin-medium);
}
#toggle-theme:focus {
outline: none;
}
button,
input {
background-color: var(--blender-button-background, var(--bg-color));
color: var(--blender-button-text, var(--text-color));
border-color: var(--blender-button-border, var(--border-color));
transition: filter 0.2s ease;
}
footer {
background-color: var(--blender-top-bar-header, var(--nav-bg-color));
text-align: right;
padding: var(--padding-tiny);
border-top: 1px solid var(--blender-tab-outline, var(--nav-border-color));
}
footer p {
margin: 0;
color: var(--blender-text, var(--nav-link-color));
margin-right: var(--margin-small);
font-size: 0.8rem;
}
/* =========== styling for code blocks and rendering =========== */
/* ================== This is auto-generated ================== */
#explanation {
font-family: "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif;
font-size: 16px;
padding: 0 26px;
line-height: 22px;
word-wrap: break-word;
margin: 0;
}
#code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color: #444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .25);
}
#code-csp-warning:hover {
text-decoration: none;
background-color: #007acc;
box-shadow: 2px 2px 2px rgba(0, 0, 0, .25);
}
#explanation.scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
#explanation.showEditorSelection .code-line {
position: relative;
}
#explanation.showEditorSelection .code-active-line:before,
#explanation.showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
#explanation.showEditorSelection li.code-active-line:before,
#explanation.showEditorSelection li.code-line:hover:before {
left: -30px;
}
#explanation a:focus,
#explanation input:focus,
#explanation select:focus,
#explanation textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
#explanation hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
#explanation h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
#explanation h1,
#explanation h2,
#explanation h3 {
font-weight: normal;
}
#explanation code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 1em;
line-height: 1.357em;
}
#explanation .wordWrap pre {
white-space: pre-wrap;
}
#explanation pre:not(.hljs),
#explanation pre.hljs code>div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
#explanation pre code {
tab-size: 4;
}
#explanation .hljs-comment,
#explanation .hljs-quote {
color: #B6B18B;
}
#explanation .hljs-variable,
#explanation .hljs-template-variable,
#explanation .hljs-tag,
#explanation .hljs-name,
#explanation .hljs-selector-id,
#explanation .hljs-selector-class,
#explanation .hljs-regexp,
#explanation .hljs-deletion {
color: #EB3C54;
}
#explanation .hljs-number,
#explanation .hljs-built_in,
#explanation .hljs-builtin-name,
#explanation .hljs-literal,
#explanation .hljs-type,
#explanation .hljs-params,
#explanation .hljs-meta,
#explanation .hljs-link {
color: #E7CE56;
}
#explanation .hljs-attribute {
color: #EE7C2B;
}
#explanation .hljs-string,
#explanation .hljs-symbol,
#explanation .hljs-bullet,
#explanation .hljs-addition {
color: #4FB4D7;
}
#explanation .hljs-title,
#explanation .hljs-section {
color: #78BB65;
}
#explanation .hljs-keyword,
#explanation .hljs-selector-tag {
color: #B45EA4;
}
#explanation .hljs {
display: block;
overflow-x: auto;
background: #1C1D21;
color: #c0c5ce;
padding: 0.5em;
}
#explanation .hljs-emphasis {
font-style: italic;
}
#explanation .hljs-strong {
font-weight: bold;
}
#explanation {
font-family: "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif, "Meiryo";
padding: 0 12px;
}
#explanation pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
border-radius: 3px;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
#explanation pre:not(.hljs) {
padding: 23px;
line-height: 19px;
}
#explanation blockquote {
background: rgba(127, 127, 127, 0.1);
border-color: rgba(0, 122, 204, 0.5);
}
#explanation .emoji {
height: 1.4em;
}
#explanation code {
font-size: 14px;
line-height: 19px;
}
#explanation :not(pre):not(.hljs)>code {
color: #C9AE75;
font-size: inherit;
}
#explanation .page {
page-break-after: always;
}
@@ -119,11 +119,9 @@ nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
.warning {
.warning,
.warning-icon {
color: var(--blender-info-warning, var(--warning-color));
margin-bottom: var(--margin-small);
padding: var(--padding-tiny);
display: none;
}
.table-description {
@@ -144,6 +142,14 @@ nav ul li a.active {
border-bottom: 1px solid var(--blender-tab-outline, var(--border-color));
}
.table-description tr:nth-child(even) {
background-color: var(--blender-even-row);
}
.table-description tr:nth-child(odd) {
background-color: var(--blender-odd-row);
}
#toggle-theme {
border: none;
background: none;
@@ -342,12 +348,6 @@ button:hover {
border-color: var(--border-color);
}
/* .gTaskInfo {
background-color: #2e2e2e;
color: #c7c7c7;
border-color: #c7c7c7;;
} */
/* Dark mode scrollbar */
:root.dark .frame::-webkit-scrollbar-thumb,
:root.dark .frame::-webkit-scrollbar-track {
@@ -465,6 +465,11 @@ button:hover {
border-color: var(--blender-tab-outline);
}
:root.blender .gtaskcellcurrent {
background-color: var(--blender-odd-row);
border-color: var(--blender-tab-outline);
}
:root.blender .gtaskheading,
:root.blender .gname,
:root.blender .gtaskname,
@@ -57,11 +57,9 @@ body {
}
#container {
position: relative;
padding: var(--margin-small);
flex: 1;
margin-top: var(--margin-medium);
margin-left: var(--margin-small);
margin-right: var(--margin-small);
margin-bottom: var(--margin-medium);
}
h3 {
@@ -105,11 +103,9 @@ nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
.warning {
.warning,
.warning-icon {
color: var(--blender-info-warning, var(--warning-color));
margin-bottom: var(--margin-small);
padding: var(--padding-tiny);
display: none;
}
#toggle-theme {
@@ -206,7 +202,11 @@ footer p {
}
.table-container {
margin-bottom: var(--margin-large);
position: absolute;
display: block;
height: calc(100% - var(--margin-small) * 2);
width: calc(100% - var(--margin-small) * 2);
overflow-x: scroll;
}
.csv-table {
@@ -299,4 +299,4 @@ button:hover {
.tabulator-menu-item:hover {
filter: brightness(1.4);
}
}
@@ -0,0 +1,146 @@
// A global variable to hold the WebSocket connection
let socket;
// Document ready function
// This function is triggered once the DOM is fully loaded.
// It sets the theme for the UI and
// establishes the WebSocket connection by calling connectSocket().
$(document).ready(function () {
var defaultTheme = "blender"; // Default theme to be applied
var theme = localStorage.getItem("theme") || defaultTheme; // Retrieve the stored theme or use the default
setTheme(theme); // Apply the theme
connectSocket();
});
// Function to establish WebSocket connection
// This function constructs the WebSocket connection URL using the SOCKET_PORT variable
// and initializes the socket connection. It also registers event handlers to listen
// for various events emitted by the server, such as 'blender_connect', 'blender_disconnect',
// 'theme_data', and 'demo_data'.
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("theme_data", handleThemeData);
socket.on("demo_data", handleDemoData);
}
// Function to handle 'blender_connect' event
// This function is triggered when a new Bonsai instance connects to the server.
// The 'blenderId' is a unique identifier assigned by the server to each Bonsai instance.
// It helps distinguish between different instances of Bonsai connected to the server.
// The 'blenderId' is used in various parts of the code to ensure that messages and data
// are correctly associated with the appropriate Bonsai instance.
function handleBlenderConnect(blenderId) {
console.log("blender connected: ", blenderId);
}
// Function to handle 'blender_disconnect' event
// This function is triggered when a Bonsai instance disconnects from the server.
// The 'blenderId' is used here to log which specific Bonsai instance has disconnected.
function handleBlenderDisconnect(blenderId) {
console.log("blender disconnected: ", blenderId);
}
// Function to handle 'demo_data' event
// This function processes the demo data received from the server.
// It extracts the message and the Bonsai instance ID ('blenderId'),
// and updates the DOM to display the message. The 'blenderId' is used
// to differentiate between messages from different Bonsai instances.
// If the message for the specific Bonsai instance is already displayed, it updates the content;
// otherwise, it creates a new element to display the message.
function handleDemoData(demoData) {
console.log(demoData);
const message = demoData["data"]["demo_message"];
const blenderId = demoData["blenderId"];
const id = "message-" + blenderId;
const messageHeader = $("#" + id);
const messageText = `Bonsai instance with ID: ${blenderId} sent the message: ${message}`;
if (messageHeader[0] === undefined) {
const newMessageElement = $("<h3>", { id: id }).text(messageText);
$("#message-container").prepend(newMessageElement);
} else {
messageHeader.text(messageText);
messageHeader.prependTo("#message-container");
}
}
// Function to send a message to the server
// This function is triggered when the user submits a message.
// It collects the message from the input field, constructs a message object,
// and emits the 'web_operator' event to the server with the message data.
// If 'blenderId' is specified, the message is sent to a specific Bonsai instance.
function sendMessage() {
const inputMessage = $("#input-message").val();
const msg = {
sourcePage: "demo",
// blenderId: BlenderId, // specify the target Bonsai instance
operator: {
type: "message",
message: inputMessage,
},
};
socket.emit("web_operator", msg);
}
function handleThemeData(themeData) {
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
if (a !== undefined) {
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return `rgb(${r}, ${g}, ${b})`;
}
function generateCssVariableRule(theme) {
let cssVariables = ":root.blender {\n";
for (const key in theme) {
const cssVariableName = `--blender-${key.replace(/_/g, "-")}`;
const cssVariableValue = arrayToRgbString(theme[key]);
cssVariables += ` ${cssVariableName}: ${cssVariableValue};\n`;
}
cssVariables += "}";
return cssVariables;
}
const cssRule = generateCssVariableRule(themeData.theme);
console.log(cssRule);
var styleElement = $("#demo-stylesheet")[0];
if (styleElement) {
var sheet = styleElement.sheet || styleElement.styleSheet;
sheet.insertRule(cssRule, sheet.cssRules.length);
}
}
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
} else if (theme === "blender") {
$("#toggle-theme").html('<i class="fas fa-adjust"></i>');
}
localStorage.setItem("theme", theme);
}
function toggleTheme() {
if ($("html").hasClass("light")) {
setTheme("dark");
} else if ($("html").hasClass("dark")) {
setTheme("blender");
} else {
setTheme("light");
}
}
@@ -5,10 +5,6 @@ let socket;
// Document ready function
$(document).ready(function () {
var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
$(":root").css("color-scheme", systemTheme);
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
@@ -46,7 +42,7 @@ function handleWebConnect() {
// Function to handle 'blender_connect' event
function handleBlenderConnect(blenderId) {
console.log("blender_connect: ", blenderId);
console.log("blender connected: ", blenderId);
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = {
shown: false,
@@ -77,7 +73,7 @@ function handleDefaultData(data) {
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender_disconnect: ", blenderId);
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeSelectOption(blenderId);
@@ -90,7 +86,7 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
console.log(data);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -101,7 +97,7 @@ function handleConnectedClients(data) {
}
function handleThemeData(themeData) {
console.log(themeData);
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -145,7 +141,7 @@ function handleDrawingsData(data) {
const hasDrawings = drawings.length > 0 || sheets.length > 0;
console.log(connectedClients);
// console.log(connectedClients);
if (connectedClients.hasOwnProperty(blenderId)) {
if (!connectedClients[blenderId].shown) {
@@ -203,11 +199,10 @@ function addSelectOption(blenderId, filename) {
function updateSelectOption(blenderId, filename) {
$("#blender-" + blenderId).text(filename);
console.log(blenderId, filename);
// console.log(blenderId, filename);
console.log($("#dropdown-menu").val());
if ($("#dropdown-menu").val() === filename) {
console.log("here");
displayDrawingsNames(blenderId, filename);
}
}
@@ -253,7 +248,7 @@ function displayDrawingsNames(blenderId, ifcFile) {
const msg = {
path: path,
};
console.log(msg);
// console.log(msg);
socket.emit("get_svg", msg);
$("li").removeClass("selected-svg");
@@ -277,6 +272,7 @@ function displayDrawingsNames(blenderId, ifcFile) {
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
@@ -1,5 +1,5 @@
// keeps track of blenders connected in form of
// shown:bool, workSchedule: {}, ganttTasks: {},
// shown:bool, workSchedule: {}, ganttTasks: {}, warning: bool
const connectedClients = {};
let socket;
@@ -19,10 +19,6 @@ const pageSizes = [
// Document ready function
$(document).ready(function () {
var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
$(":root").css("color-scheme", systemTheme);
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
@@ -47,7 +43,7 @@ function connectSocket() {
// Function to handle 'blender_connect' event
function handleBlenderConnect(blenderId) {
console.log("blender_connect: ", blenderId);
console.log("blender connected: ", blenderId);
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = {
shown: false,
@@ -63,7 +59,7 @@ function handleBlenderConnect(blenderId) {
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender_disconnect: ", blenderId);
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeGanttElement(blenderId);
@@ -76,7 +72,7 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
console.log(data);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -87,7 +83,7 @@ function handleConnectedClients(data) {
}
function handleThemeData(themeData) {
console.log(themeData);
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -135,12 +131,14 @@ function handleGanttData(data) {
ifc_file: filename,
ganttTasks: ganttTasks,
workSchedule: ganttWorkSched,
warning: false,
};
addGanttElement(blenderId, ganttTasks, ganttWorkSched, filename);
} else {
updateGanttElement(blenderId, ganttTasks, ganttWorkSched, filename);
connectedClients[blenderId].workSchedule = ganttWorkSched;
connectedClients[blenderId].ganttTasks = ganttTasks;
connectedClients[blenderId].warning = false;
}
} else {
connectedClients[blenderId] = {
@@ -148,6 +146,7 @@ function handleGanttData(data) {
ifc_file: filename,
ganttTasks: ganttTasks,
workSchedule: ganttWorkSched,
warning: false,
};
addGanttElement(blenderId, ganttTasks, ganttWorkSched, filename);
}
@@ -161,7 +160,163 @@ function handleDefaultData(data) {
}
// Function to add a new gantt with data and filename
function addGanttElement(blenderId, tasks, workSched, filename) {
function createWorkSchedDiv(blenderId, workSched) {
const workSchedDiv = $("<div></div>").attr("id", "workSched" + blenderId);
const scheduleTable = $("<table></table>")
.addClass("no-print table-description")
.attr("id", "workSchedTable-" + blenderId)
.hide();
$.each(workSched, (key, value) => {
value = value ? value : "null";
$("<tr></tr>")
.append($("<td></td>").text(key))
.append($("<td></td>").text(value))
.appendTo(scheduleTable);
});
const toggleButton = $("<button></button>")
.text("Show Schedule Info")
.addClass("btn no-print")
.on("click", function () {
scheduleTable.toggle();
const buttonText = scheduleTable.is(":visible")
? "Hide Schedule Info"
: "Show Schedule Info";
toggleButton.text(buttonText);
});
workSchedDiv.append(toggleButton);
workSchedDiv.append(scheduleTable);
return workSchedDiv;
}
function createGanttInfoDiv(blenderId, workSched) {
const ganttInfoDiv = $("<div></div>")
.addClass("gantt-info")
.attr("id", "gantt-info-" + blenderId);
const scheduleName = $("<span></span>").text("Schedule: " + workSched.Name);
const createdOn = $("<span></span>")
.text("Created: " + new Date(workSched.CreationDate).toLocaleDateString())
.css("float", "right");
ganttInfoDiv.append(scheduleName);
ganttInfoDiv.append(createdOn);
return ganttInfoDiv;
}
function createGantt(blenderId) {
const ganttDiv = $("<div></div>")
.addClass("gantt-chart")
.attr("id", "gantt-" + blenderId);
let g = new JSGantt.GanttChart(ganttDiv[0], "week");
g.setOptions({
vCaptionType: "Caption", // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: "day dd month yyyy", // Shown in tool tip box
vDayMajorDateDisplayFormat: "mon yyyy - Week ww", // Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: "dd mon", // Set format to display dates in the "Minor" header of the "Week" view
vLang: "en",
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ["Day", "Week", "Month", "Quarter"], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vShowRes: true, // Disable the resource column.
vShowComp: false, // Disable the completion column.
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
vAdditionalHeaders: {
ifcduration: { title: "Duration" },
resourceUsage: { title: "Resource Usage" },
},
vUseToolTip: true, // Disable tooltips.
vTooltipTemplate: generateTooltip,
vTotalHeight: 900,
vEventsChange: {
taskname: editValue, // if you need to use the this scope, do: editValue.bind(this)
res: editValue,
dur: editValue,
comp: editValue,
start: editValue,
end: editValue,
planstart: editValue,
planend: editValue,
cost: editValue,
additional_category: editValue,
},
});
JSGantt.addJSONTask(g, tasks);
g.setEditable(true);
connectedClients[blenderId]["gantt"] = g;
return ganttDiv;
}
function createPrint(blenderId) {
const printButton = $("<button>", {
id: "print-btn-" + blenderId,
html: "Print",
class: "btn no-print",
});
const printOptions = $("<select>", {
id: "print-options-" + blenderId,
class: "no-print",
});
$.each(pageSizes, function (index, size) {
printOptions.append(
$("<option>", {
value: size.value,
text: size.text,
})
);
});
printButton.on("click", function () {
$(".gantt-chart").removeClass("no-print");
$(".gantt-chart")
.not("#gantt-" + blenderId)
.addClass("no-print");
$(".gantt-info")
.not("#gantt-info-" + blenderId)
.addClass("no-print");
const values = printOptions.val().split(",");
const g = connectedClients[blenderId]["gantt"];
g.setEditable(false);
g.setTotalHeight("");
g.Draw();
addEventListener("afterprint", () => g.setEditable(true));
let css = `@media print {
@page {
size: ${values[0]}mm ${values[1]}mm;
}
body, p, span, h1, h2, h3, h4, h5, h6, div, a, li, td, th, * {
color: black !important;
}
}`;
g.printChart(values[0], values[1], css);
g.setTotalHeight(900);
g.Draw();
});
return { printButton, printOptions };
}
const ganttContainer = $("<div></div>")
.addClass("gantt-container")
.attr("id", "container-" + blenderId);
@@ -170,171 +325,27 @@ function addGanttElement(blenderId, tasks, workSched, filename) {
.attr("id", "title-" + blenderId)
.text(filename)
.addClass("no-print")
.css("margin-bottom", "10px");
.css("margin-bottom", "0.875rem");
const warning = $("<div></div>")
.attr("id", "warning-" + blenderId)
.html(
"&#9888; Warning: This Gantt Chart may contain outdated data due to recent changes in Blender."
)
.addClass("warning no-print");
const workSchedDiv = $("<div></div>").attr("id", "workSched" + blenderId);
const scheduleTable = $("<table></table>")
.addClass("no-print table-description")
.attr("id", "workSchedTable-" + blenderId)
.hide();
$.each(workSched, (key, value) => {
value = value ? value : "null";
$("<tr></tr>")
.append($("<td></td>").text(key))
.append($("<td></td>").text(value))
.appendTo(scheduleTable);
});
const toggleButton = $("<button></button>")
.text("Show Schedule Info")
.addClass("btn no-print")
.on("click", function () {
scheduleTable.toggle();
const buttonText = scheduleTable.is(":visible")
? "Hide Schedule Info"
: "Show Schedule Info";
toggleButton.text(buttonText);
});
var ganttInfoDiv = $("<div></div>")
.addClass("gantt-info")
.attr("id", "gantt-info-" + blenderId);
const scheduleName = $("<span></span>").text("Schedule: " + workSched.Name);
const createdOn = $("<span></span>")
.text("Created: " + new Date(workSched.CreationDate).toLocaleDateString())
.css("float", "right");
const ganttDiv = $("<div></div>")
.addClass("gantt-chart")
.attr("id", "gantt-" + blenderId);
ganttInfoDiv.append(scheduleName);
ganttInfoDiv.append(createdOn);
workSchedDiv.append(toggleButton);
workSchedDiv.append(scheduleTable);
const workSchedDiv = createWorkSchedDiv(blenderId, workSched);
const ganttInfoDiv = createGanttInfoDiv(blenderId, workSched);
const ganttDiv = createGantt(blenderId, tasks);
const { printButton, printOptions } = createPrint(blenderId);
ganttContainer.append(ganttTitle);
ganttContainer.append(warning);
ganttContainer.append(workSchedDiv);
ganttContainer.append(ganttInfoDiv);
ganttContainer.append(ganttDiv);
$("#container").append(ganttContainer);
let g = new JSGantt.GanttChart($("#gantt-" + blenderId)[0], "week");
g.setOptions({
vCaptionType: "Caption", // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: "day dd month yyyy", // Shown in tool tip box
vDayMajorDateDisplayFormat: "mon yyyy - Week ww", // Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: "dd mon", // Set format to display dates in the "Minor" header of the "Week" view
vLang: "en",
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ["Day", "Week", "Month", "Quarter"], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vShowRes: true, // Disable the resource column.
vShowComp: false, // Disable the completion column.
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
vAdditionalHeaders: {
ifcduration: { title: "Duration" },
resourceUsage: { title: "Resource Usage" },
},
vUseToolTip: true, // Disable tooltips.
vTooltipTemplate: generateTooltip,
vTotalHeight: 900,
vEventsChange: {
taskname: editValue, // if you need to use the this scope, do: editValue.bind(this)
res: editValue,
dur: editValue,
comp: editValue,
start: editValue,
end: editValue,
planstart: editValue,
planend: editValue,
cost: editValue,
additional_category: editValue,
},
});
JSGantt.addJSONTask(g, tasks);
g.setEditable(true);
g.Draw();
connectedClients[blenderId]["gantt"] = g;
let printButton = $("<button>", {
id: "print-btn-" + blenderId,
html: "Print",
class: "btn no-print",
});
let printOptions = $("<select>", {
id: "print-options-" + blenderId,
class: "no-print",
});
$.each(pageSizes, function (index, size) {
printOptions.append(
$("<option>", {
value: size.value,
text: size.text,
})
);
});
printButton.on("click", function () {
// make it only the corresponding gantt chart is printed
$(".gantt-chart").removeClass("no-print");
$(".gantt-chart")
.not("#gantt-" + blenderId)
.addClass("your-css-class");
var values = $("#print-options-" + blenderId)
.val()
.split(",");
g.setEditable(false);
g.setTotalHeight("");
g.Draw();
addEventListener("afterprint", (event) => {
g.setEditable(true);
});
let css =
"@media print {\n" +
" @page {\n" +
" size: " +
values[0] +
"mm " +
values[1] +
"mm;\n" +
" }\n" +
" /* Make all text black */\n" +
" body, p, span, h1, h2, h3, h4, h5, h6, div, a, li, td, th, * {\n" +
" color: black !important;\n" +
" }\n" +
"}";
g.printChart(values[0], values[1], css);
g.setTotalHeight(900);
g.Draw();
});
ganttContainer.append(printOptions);
ganttContainer.append(printButton);
$("#container").append(ganttContainer);
// have to call draw after appening the containers to the DOM
// otherwise the dependencies will not be displayed properly
const g = connectedClients[blenderId].gantt;
g.Draw();
}
// Function to update gantt and filename
@@ -358,7 +369,6 @@ function updateGanttElement(blenderId, tasks, workSched, filename) {
g.Draw();
$("#title-" + blenderId).text(filename);
$("#warning-" + blenderId).css("display", "none");
}
// Function to remove gantt element
@@ -367,7 +377,7 @@ function removeGanttElement(blenderId) {
}
function showWarning(blenderId, isDirty) {
$("#warning-" + blenderId).css("display", "block");
connectedClients[blenderId].warning = true;
}
// Utility function to create a tooltip for the gantt chars
@@ -390,12 +400,12 @@ function generateTooltip(task) {
// Event handlers for editing gantt table data
function editValue(list, task, event, cell, column) {
console.log("editValue function called with the following parameters:");
console.log("list:", list);
console.log("task:", task);
console.log("event:", event);
console.log("cell:", cell);
console.log("column:", column);
// console.log("editValue function called with the following parameters:");
// console.log("list:", list);
// console.log("task:", task);
// console.log("event:", event);
// console.log("cell:", cell);
// console.log("column:", column);
const ganttId = task.getGantt()["vDiv"].id;
const index = ganttId.indexOf("-") + 1;
@@ -418,11 +428,13 @@ function editValue(list, task, event, cell, column) {
value: event.target.value,
},
};
console.log("web operator: " + msg);
socket.emit("web_operator", msg);
}
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
@@ -462,12 +474,29 @@ function toggleClientList() {
const clientDiv = $("<div>")
.addClass("client")
.addClass(`client-${id}`)
.text(client.ifc_file || "Blender " + counter);
if (client.warning) {
warningIcon = $("<i>")
.addClass("fas fa-triangle-exclamation warning")
.css("margin-left", "0.5rem");
clientDiv.append(warningIcon);
}
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (client.warning) {
warningIcon = $("<i>").addClass("fa-solid fa-triangle-exclamation");
const clientWarning = $("<div>")
.addClass("client-detail warning")
.text(" Might have outdated data due to recent changes in Blender.");
clientWarning.prepend(warningIcon);
clientDetailsDiv.append(clientWarning);
}
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
@@ -519,5 +548,6 @@ function toggleClientList() {
clientList.append(clientDiv);
});
clientList.addClass("show");
}
@@ -1,14 +1,10 @@
// keeps track of blenders connected in form of
// {shown:bool, filename:string, headers:array}
// {shown:bool, filename:string, headers:array, warning: false}
const connectedClients = {};
let socket;
// Document ready function
$(document).ready(function () {
var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
$(":root").css("color-scheme", systemTheme);
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
@@ -33,10 +29,11 @@ function connectSocket() {
// Function to handle 'blender_connect' event
function handleBlenderConnect(blenderId) {
console.log("blender_connect: ", blenderId);
console.log("blender connected: ", blenderId);
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = { shown: false, ifc_file: "" };
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
@@ -44,7 +41,7 @@ function handleBlenderConnect(blenderId) {
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender_disconnect: ", blenderId);
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeTableElement(blenderId);
@@ -57,14 +54,14 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
console.log(data);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = { shown: false, ifc_file: "" };
});
}
function handleThemeData(themeData) {
console.log(themeData);
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -108,16 +105,19 @@ function handleCsvData(data) {
shown: true,
ifc_file: filename,
headers: [],
warning: false,
};
addTableElement(blenderId, csvData, filename);
} else {
updateTableElement(blenderId, csvData, filename);
connectedClients[blenderId].warning = false;
}
} else {
connectedClients[blenderId] = {
shown: true,
ifc_file: filename,
headers: [],
warning: false,
};
addTableElement(blenderId, csvData, filename);
}
@@ -135,9 +135,16 @@ function addTableElement(blenderId, csvData, filename) {
// store headers of the csv data
const firstLine = csvData.indexOf("\n");
const csvHeaders = csvData.substring(0, firstLine).split(",");
connectedClients[blenderId].headers = csvHeaders;
const hasRows = csvData.substring(firstLine + 1).trim().length > 0;
if (hasRows) {
connectedClients[blenderId].headers = csvHeaders;
} else {
connectedClients[blenderId].headers = "";
}
// console.log(connectedClients[blenderId]);
console.log(connectedClients[blenderId]);
const tableContainer = $("<div></div>")
.addClass("table-container")
.attr("id", "container-" + blenderId);
@@ -147,19 +154,11 @@ function addTableElement(blenderId, csvData, filename) {
.text(filename)
.css("margin-bottom", "10px");
const warning = $("<div></div>")
.attr("id", "warning-" + blenderId)
.html(
"&#9888; Warning: This table may contain outdated data due to recent changes in Blender."
)
.addClass("warning");
const tableDiv = $("<div></div>")
.addClass("csv-table")
.attr("id", "table-" + blenderId);
tableContainer.append(tableTitle);
tableContainer.append(warning);
tableContainer.append(tableDiv);
$("#container").append(tableContainer);
@@ -192,8 +191,8 @@ function addTableElement(blenderId, csvData, filename) {
// Toggle current column visibility
column.visible = !column.visible;
console.log(column.title, column.visible);
// Change menu item checkbox
// console.log(column.title, column.visible);
if (column.visible) {
table.showColumn(column.title);
checkbox.textContent = "\u2611"; // ☑
@@ -208,25 +207,40 @@ function addTableElement(blenderId, csvData, filename) {
return menu;
}
var containerHeight=$('.table-container').height();
var h3Height=$('.table-container>h3').height();
var table = new Tabulator("#table-" + blenderId, {
height: "400px",
placeholder: "No data to display",
resizableColumnGuide: true,
index: "GlobalId",
data: csvData,
importFormat: "csv",
autoColumns: true,
selectableRows: 1,
layout: "fitColumns",
layout: "fitDataFill",
height: containerHeight - h3Height - 20 + "px",
movableColumns: true,
// Our fields are never nested, and we use the "." character in queries
nestedFieldSeparator: false,
autoColumnsDefinitions: function (definitions) {
menu = createHeaderMenu(definitions, table);
definitions.forEach((column) => {
column.visible = true;
column.headerFilter = true;
column.headerMenu = menu;
// TODO: more user control to choose function, style at bottomCalc
column.topCalc = 'sum';
});
return definitions;
},
});
var downloadCsv = $('<a><i class="fa-solid fa-file-csv"></i></a>')
.css("margin-left", "10px")
.css("cursor", "pointer");
tableTitle.append(downloadCsv);
downloadCsv.on('click', function() { table.download("csv", "data.csv"); })
table.on("rowSelected", function (row) {
var index = row.getIndex(); // the guid of the object
@@ -258,6 +272,8 @@ function updateTableElement(blenderId, csvData, filename) {
// else, replace the whole data and column definations
const firstLine = csvData.indexOf("\n");
const newHeaders = csvData.substring(0, firstLine).split(",");
const hasRows = csvData.substring(firstLine + 1).trim().length > 0;
const sameHeaders = compareHeaders(
connectedClients[blenderId].headers,
newHeaders
@@ -269,10 +285,14 @@ function updateTableElement(blenderId, csvData, filename) {
table.replaceData(csvData, { importFormat: "csv" });
} else {
table.setData(csvData);
connectedClients[blenderId].headers = newHeaders;
if (hasRows) {
connectedClients[blenderId].headers = newHeaders;
} else {
connectedClients[blenderId].headers = "";
}
}
$("#title-" + blenderId).text(filename);
$("#warning-" + blenderId).css("display", "none");
}
}
@@ -282,12 +302,16 @@ function removeTableElement(blenderId) {
}
function showWarning(blenderId, isDirty) {
$("#warning-" + blenderId).css("display", "block");
connectedClients[blenderId].warning = true;
}
// Utility function to compare two csv header
function compareHeaders(headers1, headers2) {
if (headers1.length !== headers2.length) {
if (
headers1.length !== headers2.length ||
headers1.length == 0 ||
headers2.length == 0
) {
return false;
}
for (let i = 0; i < headers1.length; i++) {
@@ -300,6 +324,7 @@ function compareHeaders(headers1, headers2) {
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
var stylesheet = $("#tabulator-stylesheet");
if (theme === "light") {
stylesheet.attr(
@@ -349,12 +374,29 @@ function toggleClientList() {
const clientDiv = $("<div>")
.addClass("client")
.addClass(`client-${id}`)
.text(client.ifc_file || "Blender " + counter);
if (client.warning) {
warningIcon = $("<i>")
.addClass("fas fa-triangle-exclamation warning")
.css("margin-left", "0.5rem");
clientDiv.append(warningIcon);
}
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (client.warning) {
warningIcon = $("<i>").addClass("fa-solid fa-triangle-exclamation");
const clientWarning = $("<div>")
.addClass("client-detail warning")
.text(" Might have outdated data due to recent changes in Blender.");
clientWarning.prepend(warningIcon);
clientDetailsDiv.append(clientWarning);
}
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
@@ -397,5 +439,6 @@ function toggleClientList() {
clientList.append(clientDiv);
});
clientList.addClass("show");
}
@@ -0,0 +1,268 @@
<!-- this is a demo web UI page -->
<!DOCTYPE html>
<html lang="en" class="blender">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bonsai Web UI</title>
<!-- here we request the CSS file from the server, -->
<!-- using registered static path in the server -->
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
<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://cdn.socket.io/4.0.0/socket.io.min.js"
></script>
<script>
// here we define the port that the server is listening on.
// so that we can connect to the websocket server and request static files
// this is set when the HTML file is served by the server when opening the web UI URL.
var SOCKET_PORT = {{port}};
</script>
<!-- here we request the JS file from the server-->
<!-- using registered static path in the server -->
<script defer src="./static/js/demo.js"></script>
</head>
<body>
<!-- the navigation bar at the top of the page. -->
<nav>
<img
src="https://bonsaibim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul>
<li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
<li>
<a><i class="fa-solid fa-camera"></i> BCF Topics</a>
</li>
<li>
<a><i class="fa-solid fa-square-check"></i> IDS Audits</a>
</li>
<li>
<a><i class="fa-solid fa-hotel"></i> Facility Management</a>
</li>
</ul>
<button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav>
<!-- the main page container. -->
<div id="container">
<div id="message-container">
<!-- Here we have a text input and a button that calls the SendMessage() function-->
<input type="text" id="input-message" placeholder="Send a Message" />
<button onclick="sendMessage()">Send Message to Bonsai</button>
</div>
<!-- The following is the explanation part of the demo page. -->
<!-- I suggest reading it from the demo page iteself :) -->
<!-- we need to stop formating because formatiing messes up pre tag spacing -->
<!-- which in turn messes up code blocks display-->
<!-- prettier-ignore-start -->
<div id="explanation">
<h1 id="welcome-to-the-web-ui-demo-page">Welcome to the web UI demo page!</h1>
<p>This demo showcases how Bonsai's Web UI interacts with a WebSocket server. The demo illustrates how the Web UI establishes a connection, exchanges data with the server, and updates the UI dynamically based on incoming data.</p>
<p>You can use the above textbox to send a message to Bonsai which will be printed to the console!</p>
<p>If you prefer, you can explore the source code directly in the data/webui directory:</p>
<ul>
<li>sioserver.py</li>
<li>templates/demo.html</li>
<li>static/js/demo.js</li>
</ul>
<p>Each file contains detailed comments to help you understand the code. Alternatively, continue reading this demo page to understand how the Web UI operates</p>
<h2 id="webscoket-server-sioserverpy">WebScoket Server (sioserver.py)</h2>
<p>In the websocket server, there are two key namespaces.</p>
<ul>
<li>Blender Namespace: Connects all Bonsai instances.</li>
<li>Web Namespace: Connects all Web UI clients.</li>
</ul>
<p>These namespaces are attached to the WebSocket server, allowing event-driven communication.</p>
<pre class="hljs"><code><div><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebNamespace</span><span class="hljs-params">(socketio.AsyncNamespace)</span>:</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_connect</span><span class="hljs-params">(self, sid, environ)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_disconnect</span><span class="hljs-params">(self, sid)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_web_operator</span><span class="hljs-params">(self, sid, data)</span>:</span>
...
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlenderNamespace</span><span class="hljs-params">(socketio.AsyncNamespace)</span>:</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_connect</span><span class="hljs-params">(self, sid, environ)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_disconnect</span><span class="hljs-params">(self, sid)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_demo_data</span><span class="hljs-params">(self, sid, data)</span>:</span>
...
</div></code></pre>
<p>Each namespace defines event handlers that are triggered when a connected client emits specific events. For example, the on_demo_data function is invoked when a client emits the demo_data event.</p>
<p>The server also defines routes, such as the /demo URL, which are tied to specific request handlers.</p>
<pre class="hljs"><code><div><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">demo</span><span class="hljs-params">(request)</span>:</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"templates/demo.html"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
template = f.read()
html_content = pystache.render(template, {<span class="hljs-string">"port"</span>: sio_port, <span class="hljs-string">"version"</span>: bonsai_version})
<span class="hljs-keyword">return</span> web.Response(text=html_content, content_type=<span class="hljs-string">"text/html"</span>)
app.router.add_get(<span class="hljs-string">"/demo"</span>, demo)
</div></code></pre>
<p>In this example, when the /demo URL is accessed, the server reads the demo.html template, renders it with variables like port and version, and returns the fully rendered HTML to the client's browser.</p>
<h2 id="html-file-templatesdemohtml">HTML File (templates/demo.html)</h2>
<p>When the Web UI URL is visited, the server follows these steps:</p>
<ul>
<li>Retrieves the HTML file from the templates directory.</li>
<li>uses pystache to render variables, such as SOCKET_PORT, into the HTML template.</li>
<li>Sends the fully rendered HTML page to the web browser.</li>
</ul>
<p>The SOCKET_PORT variable is injected into the HTML and later used by the JavaScript file to establish a WebSocket connection.</p>
<pre class="hljs"><code><div><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="actionscript">
<span class="hljs-keyword">var</span> SOCKET_PORT = {{port}};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</div></code></pre>
<p>After the HTML page loads, the browser automatically requests static files, such as CSS and JavaScript, from the static directory specified in the sioserver.py file.</p>
<pre class="hljs"><code><div><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/static/css/demo.css"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"demo-stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">defer</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./static/js/demo.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</div></code></pre>
<h2 id="javascript-file-staticjsdemojs">JavaScript File (static/js/demo.js)</h2>
<p>The JavaScript file is responsible for establishing and managing the WebSocket connection. It starts by ensuring the DOM is fully loaded before executing any scripts.</p>
<pre class="hljs"><code><div><span class="hljs-keyword">let</span> socket;
$(<span class="hljs-built_in">document</span>).ready(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">var</span> defaultTheme = <span class="hljs-string">"blender"</span>; <span class="hljs-comment">// Default theme to be applied</span>
<span class="hljs-keyword">var</span> theme = localStorage.getItem(<span class="hljs-string">"theme"</span>) || defaultTheme; <span class="hljs-comment">// Retrieve the stored theme or use the default</span>
setTheme(theme); <span class="hljs-comment">// Apply the theme</span>
connectSocket(); <span class="hljs-comment">// Establish WebSocket connection</span>
});
</div></code></pre>
<p>Upon page load, the script sets the theme (either from local storage or the default) and then calls connectSocket function to initiate the WebSocket connection.</p>
<h3 id="establishing-websocket-connection">Establishing WebSocket connection</h3>
<p>The connectSocket function constructs the WebSocket connection URL using the SOCKET_PORT variable and then initializes the connection.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">connectSocket</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> url = <span class="hljs-string">"ws://localhost:"</span> + SOCKET_PORT + <span class="hljs-string">"/web"</span>;
socket = io(url);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"socket: "</span>, socket);
<span class="hljs-comment">// Register socket event handlers</span>
socket.on(<span class="hljs-string">"blender_connect"</span>, handleBlenderConnect);
socket.on(<span class="hljs-string">"blender_disconnect"</span>, handleBlenderDisconnect);
socket.on(<span class="hljs-string">"theme_data"</span>, handleThemeData);
socket.on(<span class="hljs-string">"demo_data"</span>, handleDemoData);
}
</div></code></pre>
<p>The socket variable is used to manage the WebSocket connection within the web namespace. After establishing the connection, the script registers event handlers for specific events emitted by the server.</p>
<h3 id="receiving-data-from-bonsai">Receiving Data from Bonsai</h3>
<p>Event handlers process the data received from the server and manipulate the DOM to reflect the changes on the webpage. For instance, the handleDemoData function processes the demo_data event and updates the demo content on the page.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleDemoData</span>(<span class="hljs-params">demoData</span>) </span>{
<span class="hljs-built_in">console</span>.log(demoData);
<span class="hljs-keyword">const</span> message = demoData[<span class="hljs-string">"data"</span>][<span class="hljs-string">"demo_message"</span>];
<span class="hljs-keyword">const</span> blenderId = demoData[<span class="hljs-string">"blenderId"</span>];
<span class="hljs-keyword">const</span> id = <span class="hljs-string">"message-"</span> + blenderId;
<span class="hljs-keyword">const</span> messageHeader = $(<span class="hljs-string">"#"</span> + id);
<span class="hljs-keyword">const</span> messageText = <span class="hljs-string">`Bonsai instance with ID: <span class="hljs-subst">${blenderId}</span> sent the message: <span class="hljs-subst">${message}</span>`</span>;
<span class="hljs-keyword">if</span> (messageHeader[<span class="hljs-number">0</span>] === <span class="hljs-literal">undefined</span>) {
<span class="hljs-keyword">const</span> newMessageElement = $(<span class="hljs-string">"&lt;h3&gt;"</span>, { <span class="hljs-attr">id</span>: id }).text(messageText);
newMessageElement.prependTo(<span class="hljs-string">"#message-container"</span>);
} <span class="hljs-keyword">else</span> {
messageHeader.text(messageText);
messageHeader.prependTo(<span class="hljs-string">"#message-container"</span>);
}
}
</div></code></pre>
<p>we use the BlenderId, which is a unique identifier given for by the server for each client connedted to it, to differentiate between different Bonsai instances.</p>
<p>This functionality is repeated for other event handlers, such as handleBlenderConnect, handleBlenderDisconnect, and handleThemeData, where specific actions are taken based on the event type and the data provided by the server.</p>
<h3 id="sending-data-to-bonsai">Sending Data to Bonsai</h3>
<p>Data is sent to the server in response to DOM events, such as onclick. These events trigger functions that use the socket variable to emit an event to the server, passing the appropriate data for the specific web operator.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SendMessage</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> inputMessage = $(<span class="hljs-string">"#input-message"</span>).val();
<span class="hljs-keyword">const</span> msg = {
<span class="hljs-attr">sourcePage</span>: <span class="hljs-string">"demo"</span>,
<span class="hljs-comment">// blenderId: BlenderId,</span>
<span class="hljs-attr">operator</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"message"</span>,
<span class="hljs-attr">message</span>: inputMessage,
},
};
socket.emit(<span class="hljs-string">"web_operator"</span>, msg);
}
</div></code></pre>
<p>In this example, the blenderId field specifies which Bonsai instance should receive the operator. If blenderId is not set, the operator is broadcast to all instances.</p>
<p>The server forwards this operator to the appropriate Bonsai instance, where it is processed by functions like sio_listen_web_operator and check_operator_queue in tool/web.py.</p>
<h2 id="web-tool-toolwebpy">Web Tool (tool/web.py)</h2>
<p>the sio_listen_web_operator function is automatically called when the event web_operator is emitted to Bonsai. It takes the web operator and attempts to put it in a web operators queue.</p>
<pre class="hljs"><code><div><span class="hljs-meta">@classmethod</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sio_listen_web_operator</span><span class="hljs-params">(cls, data)</span>:</span>
<span class="hljs-keyword">try</span>:
web_operator_queue.put_nowait(data)
<span class="hljs-keyword">except</span> queue.Full:
<span class="hljs-keyword">pass</span>
</div></code></pre>
<p>then the check_operator_queue function is called by a timer that is run every second to retrieve operators from the queue and handle them</p>
<pre class="hljs"><code><div><span class="hljs-meta">@classmethod</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">check_operator_queue</span><span class="hljs-params">(cls)</span>:</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> bpy.context.scene.WebProperties.is_connected:
<span class="hljs-keyword">with</span> web_operator_queue.mutex:
web_operator_queue.queue.clear()
<span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># unregister timer if not connected</span>
<span class="hljs-keyword">while</span> <span class="hljs-keyword">not</span> web_operator_queue.empty():
operator = web_operator_queue.get_nowait()
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> operator:
<span class="hljs-keyword">continue</span>
<span class="hljs-keyword">if</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"csv"</span>:
cls.handle_csv_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"gantt"</span>:
cls.handle_gantt_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"drawings"</span>:
cls.handle_drawings_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"demo"</span>:
message = operator[<span class="hljs-string">"operator"</span>][<span class="hljs-string">"message"</span>]
print(<span class="hljs-string">f"Message from demo page: <span class="hljs-subst">{message}</span>"</span>)
<span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span>
</div></code></pre>
<p>Here we check the source page of the operator and call the appropriate handler for that page.</p>
</div>
<!-- prettier-ignore-end -->
</div>
<footer>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
@@ -54,12 +54,12 @@
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/gantt"
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="drawings" class="active"
<a href="/documentation" class="active"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
@@ -149,7 +149,7 @@
</div>
</div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
<link
@@ -36,12 +36,12 @@
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/gantt" class="active"
<a href="/sequencing" class="active"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="drawings"
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
@@ -60,16 +60,20 @@
</button>
</nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
<button
id="show-connected-button"
class="no-print"
onclick="toggleClientList()"
>
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
<div id="client-list" class="no-print"></div>
</div>
<div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
<link
rel="stylesheet"
@@ -45,12 +45,12 @@
>
</li>
<li>
<a href="/gantt"
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="drawings"
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
@@ -78,7 +78,7 @@
</div>
<div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
+11 -3
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import bpy
import uuid
@@ -117,13 +118,18 @@ class IfcStore:
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
except:
if os.path.exists(IfcStore.cache_path):
os.remove(IfcStore.cache_path)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
except:
return
else:
@@ -228,7 +234,9 @@ class IfcStore:
IfcStore.commit_link_element(data)
@staticmethod
def link_element(element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
def link_element(
element: ifcopenshell.entity_instance, obj: Union[IFC_CONNECTED_TYPE, tool.Geometry.TYPES_WITH_MESH_PROPERTIES]
) -> None:
# Please use tool.Ifc.link() instead of this method. We want to
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
+7 -21
View File
@@ -785,7 +785,9 @@ class IfcImporter:
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, placement_matrix))
self.link_element(product, obj)
def get_pointcloud_representation(self, product):
def get_pointcloud_representation(
self, product: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
if hasattr(product, "Representation") and hasattr(product.Representation, "Representations"):
representations = product.Representation.Representations
elif hasattr(product, "RepresentationMaps") and hasattr(product.RepresentationMaps, "RepresentationMaps"):
@@ -825,28 +827,12 @@ class IfcImporter:
def create_pointcloud(
self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
placement_matrix = self.get_element_matrix(product)
vertex_list = []
for item in representation.Items:
if item.is_a("IfcCartesianPointList3D"):
vertex_list.extend(
mathutils.Vector(list(coordinates)) * self.unit_scale for coordinates in item.CoordList
)
elif item.is_a("IfcCartesianPointList2D"):
vertex_list.extend(
mathutils.Vector(list(coordinates)).to_3d() * self.unit_scale for coordinates in item.CoordList
)
elif item.is_a("IfcCartesianPoint"):
vertex_list.append(mathutils.Vector(list(item.Coordinates)) * self.unit_scale)
if len(vertex_list) == 0:
return None
mesh_name = tool.Geometry.get_representation_name(representation)
mesh = bpy.data.meshes.new(mesh_name)
mesh.from_pydata(vertex_list, [], [])
mesh = tool.Loader.create_point_cloud_mesh(representation)
if mesh is None:
return
tool.Ifc.link(representation, mesh)
placement_matrix = self.get_element_matrix(product)
obj = bpy.data.objects.new(tool.Loader.get_name(product), mesh)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, placement_matrix))
self.link_element(product, obj)
@@ -32,7 +32,6 @@ classes = (
operator.AddBcfViewpoint,
operator.BCFFileHandlerOperator,
operator.BIM_FH_import_bcf,
operator.EditBcfAuthor,
operator.EditBcfComment,
operator.EditBcfLabels,
operator.EditBcfProjectName,
+9 -1
View File
@@ -46,7 +46,15 @@ class BcfStore:
@classmethod
def set(cls, bcfxml: Union[bcf.bcfxml.BcfXml, None], filepath: str) -> None:
cls.bcfxml = bcfxml
bpy.context.scene.BCFProperties.bcf_file = filepath
props = bpy.context.scene.BCFProperties
props.bcf_file = filepath
# Set bcf_version prop on load.
if filepath or bcfxml:
bcfxml = cls.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props.bcf_version = "2" if bcf_v2 else "3"
@classmethod
def set_by_filepath(cls, filepath: str) -> None:
File diff suppressed because it is too large Load Diff
+29 -9
View File
@@ -59,11 +59,6 @@ def updateBcfProjectName(self, context):
bpy.ops.bim.edit_bcf_project_name()
def updateBcfAuthor(self, context):
if bcfstore.BcfStore.get_bcfxml():
bpy.ops.bim.edit_bcf_author()
def updateBcfTopicName(self, context):
if bcfstore.BcfStore.get_bcfxml():
bpy.ops.bim.edit_bcf_topic_name()
@@ -189,11 +184,36 @@ class BcfTopic(PropertyGroup):
is_editable: BoolProperty(name="Edit Topic Attributes", default=False, update=updateBcfTopicIsEditable)
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
props = self
active_topic = props.active_topic
active_related_topics = active_topic.related_topics.keys()
enum_items = []
i = 0
for t in props.topics:
if t.name == active_topic.name:
continue
if t.name in active_related_topics:
continue
enum_items.append((t.name, t.title, t.description))
return enum_items
class BCFProperties(PropertyGroup):
bcf_file: StringProperty(name="BCF File")
bcf_version: EnumProperty(
name="BCF Version",
description="Currently loaded BCF project version / BCF version to use for created projects",
items=[("2", "v2.1", ""), ("3", "v3.0", "")],
default="3",
)
comment_text_width: IntProperty(name="Comment Text Width", default=40)
name: StringProperty(default="", name="Project Name", update=updateBcfProjectName)
author: StringProperty(default="john@doe.com", name="Author Email", update=updateBcfAuthor)
author: StringProperty(
default="john@doe.com",
name="Author Email",
description="Author name that will be used for added comments, topics",
)
topics: CollectionProperty(name="BCF Topics", type=BcfTopic)
active_topic_index: IntProperty(name="Active BCF Topic Index", update=refreshBcfTopic)
file_reference: StringProperty(default="", name="Reference")
@@ -203,14 +223,15 @@ class BCFProperties(PropertyGroup):
label: StringProperty(
default="", name="Label", search=lambda s, c, t: get_extensions_items(s, c, t, extensions_attr="topic_labels")
)
bim_snippet_reference: StringProperty(default="", name="Reference")
bim_snippet_reference: StringProperty(default="", name="Reference", description="URI or filepath to BimSnippet")
bim_snippet_type: StringProperty(
default="", name="Type", search=lambda s, c, t: get_extensions_items(s, c, t, extensions_attr="snippet_types")
)
bim_snippet_schema: StringProperty(default="", name="Schema")
document_reference: StringProperty(default="", name="Referenced Document")
document_reference_description: StringProperty(default="", name="Description")
related_topic: StringProperty(name="Related Topic")
document_description: StringProperty(default="", name="Document Description")
related_topic: EnumProperty(name="Related Topic", items=get_related_topics)
comment: StringProperty(default="", name="Comment")
has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False)
@@ -225,7 +246,6 @@ class BCFProperties(PropertyGroup):
self.bim_snippet_schema = ""
self.document_reference = ""
self.document_reference_description = ""
self.related_topic = ""
self.comment = ""
self.has_related_viewpoint = False
+34 -9
View File
@@ -44,12 +44,19 @@ class BIM_PT_bcf(Panel):
row = layout.row(align=True)
row.operator("bim.new_bcf_project", text="New Project")
row.operator("bim.load_bcf_project", text="Load Project")
layout.prop(props, "bcf_version")
return
row = layout.row(align=True)
row.operator("bim.save_bcf_project", icon="EXPORT", text="Save Project")
op = row.operator("bim.save_bcf_project", icon="FILE_TICK", text="Save Current Project")
op.save_current_bcf = True
row.operator("bim.save_bcf_project", icon="EXPORT", text="Save Project As...")
row.operator("bim.unload_bcf_project", text="", icon="CANCEL")
row = layout.row()
row.prop(props, "bcf_version", emboss=False)
row.enabled = False
row = layout.row()
row.prop(props, "name")
@@ -116,6 +123,10 @@ class BIM_PT_bcf_metadata(Panel):
bl_parent_id = "BIM_PT_bcf"
def draw(self, context):
# TODO: in bcf v3 documents can be added to the bcf
# without adding them to specific topic.
# Currently we just handle it the same way as in v2
# documents are accessed and managed from topics.
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
@@ -127,6 +138,8 @@ class BIM_PT_bcf_metadata(Panel):
if not bcfxml or props.active_topic_index >= len(props.topics):
layout.label(text="No BCF project is loaded")
return
bcf_verison = bcfxml.version.version_id or ""
bcf_v3 = bcf_verison.startswith("3")
topic = props.active_topic
bcf_topic = bcfxml.topics[topic.name]
@@ -195,7 +208,7 @@ class BIM_PT_bcf_metadata(Panel):
row.operator("bim.add_bcf_label")
layout.label(text="BIM Snippet:")
if topic.bim_snippet.schema:
if topic.bim_snippet.reference:
row = layout.row(align=True)
row.prop(topic.bim_snippet, "type", emboss=False)
if topic.bim_snippet.schema:
@@ -224,7 +237,7 @@ class BIM_PT_bcf_metadata(Panel):
for index, doc in enumerate(topic.document_references):
box = self.layout.box()
row = box.row(align=True)
row.prop(doc, "reference")
row.prop(doc, "reference", emboss=False)
if doc.is_external:
row.operator("bim.open_uri", icon="URL", text="").uri = doc.reference
else:
@@ -234,12 +247,17 @@ class BIM_PT_bcf_metadata(Panel):
row.operator("bim.remove_bcf_document_reference", icon="X", text="").index = index
row = box.row(align=True)
row.prop(doc, "description")
row.prop(doc, "description", emboss=False)
row = layout.row(align=True)
row.prop(props, "document_reference")
row.operator("bim.select_bcf_document_reference", icon="FILE_FOLDER", text="")
row = layout.row()
row.prop(props, "document_reference_description")
row.prop(
props, "document_reference_description", text="Reference Description" if bcf_v3 else "Document Description"
)
if bcf_v3:
row = layout.row()
row.prop(props, "document_description")
row = layout.row()
row.operator("bim.add_bcf_document_reference")
@@ -254,10 +272,14 @@ class BIM_PT_bcf_metadata(Panel):
row.operator("bim.remove_bcf_related_topic", icon="X", text="").index = index
except KeyError:
pass
row = layout.row()
row.prop(props, "related_topic")
row = layout.row()
row.operator("bim.add_bcf_related_topic")
if len(props.topics) == len(topic.related_topics) + 1:
layout.label(text="No topics to add as related.")
else:
row = layout.row()
row.prop(props, "related_topic")
row = layout.row()
row.operator("bim.add_bcf_related_topic")
class BIM_PT_bcf_comments(Panel):
@@ -293,6 +315,9 @@ class BIM_PT_bcf_comments(Panel):
author_text = "*{} ({})".format(comment.modified_author, comment.modified_date)
row = box.row(align=True)
row.label(text=author_text, icon="WORDWRAP_ON")
if comment.viewpoint:
op = row.operator("bim.activate_bcf_viewpoint", icon="SCENE", text="")
op.viewpoint_guid = comment.viewpoint
row.prop(
comment, "is_editable", icon="CHECKMARK" if comment.is_editable else "GREASEPENCIL", icon_only=True
)
+1 -1
View File
@@ -254,7 +254,7 @@ class ExportIfcCsv(bpy.types.Operator):
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
if props.format == "web":
if not context.scene.WebProperties.is_running:
if not context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server()
tool.Web.send_webui_data(data=ifc_csv.dataframe.to_csv(index=False), data_key="csv_data", event="csv_data")
+6 -2
View File
@@ -91,7 +91,7 @@ class CsvProperties(PropertyGroup):
("web", "web", ""),
],
name="Output format",
default="csv",
default="web",
)
csv_custom_delimiter: StringProperty(default="", name="Custom Delimiter")
should_show_settings: BoolProperty(default=False, name="Show Settings")
@@ -99,4 +99,8 @@ class CsvProperties(PropertyGroup):
should_show_group: BoolProperty(default=False, name="Show Grouping")
should_show_summary: BoolProperty(default=False, name="Show Summary")
should_show_formatting: BoolProperty(default=False, name="Show Formatting")
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
should_load_from_memory: BoolProperty(
default=False,
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
@@ -90,11 +90,18 @@ class ConvertToBlender(bpy.types.Operator):
def execute(self, context):
for obj in bpy.data.objects:
if obj.library:
continue
if obj.type in {"MESH", "EMPTY"}:
tool.Ifc.unlink(obj=obj)
if obj.data:
obj.data.BIMMeshProperties.ifc_definition_id = 0
data = obj.data
if tool.Geometry.has_mesh_properties(data):
if data.library:
continue
data.BIMMeshProperties.ifc_definition_id = 0
for material in bpy.data.materials:
if material.library:
continue
tool.Ifc.unlink(obj=material)
context.scene.BIMProperties.ifc_file = ""
context.scene.BIMDebugProperties.attributes.clear()
@@ -36,8 +36,10 @@ from . import ui, prop, operator
classes = (
operator.DemonstrateHelloWorld,
operator.DemonstrateRenameProject,
operator.SendWebUiDemoMessage,
prop.BIMDemoProperties,
ui.BIM_PT_demo,
ui.BIM_PT_webui_demo,
)
@@ -76,3 +76,18 @@ class DemonstrateRenameProject(bpy.types.Operator, tool.Ifc.Operator):
# properties (such as an input field) or data from the scene (like the
# actively selected object).
core.demonstrate_rename_project(tool.Ifc, tool.Demo, name=bpy.context.scene.BIMDemoProperties.name)
class SendWebUiDemoMessage(bpy.types.Operator):
bl_idname = "bim.send_webui_demo_message"
bl_label = "Send Web UI Demo Message"
bl_description = "Sends a demo message to the currently connected Web UI"
def execute(self, context):
# First, we need to make sure that we are connected to the Web UI
# We do that by checking the Web module properties' is_connected prop
# and calling the operator connect_websocket_server if we aren't connected to a Web UI
if not context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="demo")
core.send_webui_demo_message(tool.Web, message=bpy.context.scene.BIMDemoProperties.webui_message)
return {"FINISHED"}
@@ -59,3 +59,4 @@ class BIMDemoProperties(PropertyGroup):
# an input text field in the ui.py.
message: StringProperty(name="Message")
show_hints: BoolProperty(name="Show Hints", default=False)
webui_message: StringProperty(name="Web UI Message", default="Hello, Web UI!")
+24
View File
@@ -130,3 +130,27 @@ class BIM_PT_demo(bpy.types.Panel):
if self.props.show_hints:
row = self.layout.row()
row.label(text="Name cannot be blank!")
class BIM_PT_webui_demo(bpy.types.Panel):
bl_label = "Web UI Demo"
bl_idname = "BIM_PT_webui_demo"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
# we specify that this panel is closed by default
bl_options = {"DEFAULT_CLOSED"}
# here we reference the demo panel to create a sub panel
# for the web UI demo
bl_parent_id = "BIM_PT_demo"
def draw(self, context):
self.props = context.scene.BIMDemoProperties
row = self.layout.row()
# we create an input field for the property webui_message
row.prop(self.props, "webui_message")
row = self.layout.row()
# here we create a button referencing the operator send_webui_message
row.operator("bim.send_webui_demo_message")
@@ -31,6 +31,7 @@ classes = (
operator.LoadParentDocument,
operator.LoadProjectDocuments,
operator.RemoveDocument,
operator.SelectDocumentObjects,
operator.UnassignDocument,
prop.Document,
prop.BIMDocumentProperties,
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell.api
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.document as core
from bonsai.bim.ifc import IfcStore
@@ -149,3 +150,25 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if element:
core.unassign_document(tool.Ifc, product=element, document=document)
class SelectDocumentObjects(bpy.types.Operator):
bl_idname = "bim.select_document_objects"
bl_label = "Select Document Objects"
bl_options = {"REGISTER", "UNDO"}
document: bpy.props.IntProperty(name="Document ID", default=0)
def execute(self, context):
if not self.document or not (relating_document := tool.Ifc.get_entity_by_id(self.document)):
self.report({"INFO"}, f"No document found by id '{self.document}'.")
return {"FINISHED"}
i = 0
for element in ifcopenshell.util.element.get_referenced_elements(relating_document):
obj = tool.Ifc.get_object(element)
if not obj or obj not in context.selectable_objects:
continue
obj.select_set(True)
i += 1
self.report({"INFO"}, f"{i} objects selected.")
return {"FINISHED"}
@@ -66,6 +66,9 @@ class BIM_PT_documents(Panel):
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
ifc_definition_id
)
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
@@ -202,7 +202,7 @@ def format_distance(
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
if feet:
if feet is not None:
tx_dist += str(feet) + "'"
if feet and add_inches:
tx_dist += " - "
@@ -68,6 +68,8 @@ def get_location_hint(self, context):
def update_diagram_scale(self, context):
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -94,6 +96,8 @@ def update_diagram_scale(self, context):
def update_is_nts(self, context):
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -220,6 +224,8 @@ def update_has_annotation(self, context):
def update_layer(self, context, name, value):
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -400,6 +406,7 @@ class BIMCameraProperties(PropertyGroup):
filter_mode: StringProperty(name="Filter Mode", default="NONE")
include_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Include Filter")
exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter")
update_props: BoolProperty(name="Enable Props Auto Update", default=True)
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
@@ -184,6 +184,8 @@ def create_annotation():
create_annotation_occurrence(bpy.context)
else:
object_type = props.object_type
if not bpy.ops.bim.add_annotation.poll():
return
bpy.ops.bim.add_annotation(
object_type=object_type, data_type=tool.Drawing.ANNOTATION_TYPES_DATA[object_type][-1]
)
+5 -1
View File
@@ -41,7 +41,11 @@ def get_engine(self, context):
class BIMFMProperties(PropertyGroup):
ifc_files: PointerProperty(type=MultipleFileSelect)
spreadsheet_files: CollectionProperty(name="Spreadsheets", type=StrProperty)
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
should_load_from_memory: BoolProperty(
default=False,
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
engine: EnumProperty(items=get_engine, name="Engine", options=set())
format: EnumProperty(
items=[
@@ -364,7 +364,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# TODO: write unit tests to see how this bulk operation handles
# contradictory ifc_representation_class values and when
# ifc_representation_class is IfcTextLiteral
if not obj.data:
data = obj.data
if not tool.Geometry.is_data_supported_for_adding_representation(data):
continue
self.update_obj_mesh_representation(context, obj)
tool.Ifc.finish_edit(obj)
@@ -1831,6 +1832,11 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
props.items.clear()
def add_tag(item, tag: str) -> None:
if item.tags:
item.tags += ","
item.tags += tag
if bpy.context.active_object.data and hasattr(bpy.context.active_object.data, "BIMMeshProperties"):
active_representation_id = bpy.context.active_object.data.BIMMeshProperties.ifc_definition_id
element = tool.Ifc.get().by_id(active_representation_id)
@@ -1862,6 +1868,10 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
shape_aspect = inverse.OfShapeAspect[0]
new.shape_aspect = shape_aspect.Name
new.shape_aspect_id = shape_aspect.id()
elif inverse.is_a("IfcIndexedTextureMap"):
add_tag(new, "UV")
elif inverse.is_a("IfcIndexedColourMap"):
add_tag(new, "Colour")
# sort created items
sorted_items = sorted(props.items[:], key=lambda i: (not i.shape_aspect, i.shape_aspect))
@@ -1909,7 +1919,7 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.enable_editing_representation_items()
def poll_editing_representaiton_item_style(cls, context):
def poll_editing_representation_item_style(cls, context):
if not (obj := getattr(context, "active_object", None)):
return False
props = obj.BIMGeometryProperties
@@ -1944,7 +1954,7 @@ class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
return poll_editing_representation_item_style(cls, context)
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
@@ -1994,30 +2004,76 @@ class DisableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operato
class UnassignRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_representation_item_style"
bl_label = "Unassign Representation Item Style"
bl_description = "Will remove the style from all selected objects"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
return poll_editing_representation_item_style(cls, context)
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
props.is_editing_item_style = False
active_obj = context.active_object
active_props = active_obj.BIMGeometryProperties
active_props.is_editing_item_style = False
# get active representation item
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = tool.Ifc.get_entity_by_id(representation_item_id)
if not representation_item:
self.report({"ERROR"}, f"Couldn't find representation item by id {representation_item_id}.")
# Get active representation item
active_representation_item_id = active_props.items[active_props.active_item_index].ifc_definition_id
active_representation_item = tool.Ifc.get_entity_by_id(active_representation_item_id)
if not active_representation_item:
self.report({"ERROR"}, f"Couldn't find representation item by id {active_representation_item_id}.")
return {"CANCELLED"}
tool.Style.assign_style_to_representation_item(representation_item, None)
tool.Geometry.reload_representation(obj)
# reload items ui
# Retrieve styles applied to the active representation item
active_styles = set()
if hasattr(active_representation_item, 'StyledByItem'):
for styled_by_item in active_representation_item.StyledByItem:
if hasattr(styled_by_item, 'Styles'):
active_styles.update(styled_by_item.Styles)
if not active_styles:
self.report({"ERROR"}, "Couldn't find any styles associated with the active representation item.")
return {"CANCELLED"}
# Unassign matching styles from the active object itself
for style in active_styles:
tool.Style.assign_style_to_representation_item(active_representation_item, None)
tool.Geometry.reload_representation(active_obj)
break # No need to check further if one matching style is found
# Iterate over selected objects and unassign matching styles
for obj in context.selected_objects:
if obj == active_obj:
continue # Skip the active object itself
# Get the IFC entity directly from the object
element = tool.Ifc.get_entity(obj)
if not element:
continue # Skip if no IFC entity is found
representation_item_id = element.Representation.Representations[0].Items[0].id()
representation_item = tool.Ifc.get_entity_by_id(representation_item_id)
if representation_item:
# Retrieve styles for the current representation item
styles = set()
if hasattr(representation_item, 'StyledByItem'):
for styled_by_item in representation_item.StyledByItem:
if hasattr(styled_by_item, 'Styles'):
styles.update(styled_by_item.Styles)
# Unassign matching styles
for style in styles:
if style in active_styles:
tool.Style.assign_style_to_representation_item(representation_item, None)
tool.Geometry.reload_representation(obj)
break # No need to check further if one matching style is found
# Reload UI items
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_representation_item_shape_aspect"
@@ -97,6 +97,7 @@ class RepresentationItem(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
shape_aspect: StringProperty(name="Shape Aspect")
shape_aspect_id: IntProperty(name="Shape Aspect IFC ID")
tags: StringProperty(name="Tags")
class ShapeAspect(PropertyGroup):
@@ -192,6 +192,7 @@ class BIM_PT_representation_items(Panel):
RepresentationItemsData.load()
props = context.active_object.BIMGeometryProperties
layout = self.layout
row = self.layout.row(align=True)
row.label(text=f"{RepresentationItemsData.data['total_items']} Items Found")
@@ -231,6 +232,19 @@ class BIM_PT_representation_items(Panel):
row = self.layout.row()
row.label(text=active_item.layer or "No Presentation Layer", icon="STICKY_UVS_LOC")
if active_item.name.endswith("FaceSet"):
if "UV" in active_item.tags:
text = "Has UV mapping"
else:
text = "Has no UV mapping"
layout.label(text=text, icon="UV")
if "Colour" in active_item.tags:
text = "Has colour mapping"
else:
text = "Has no colour mapping"
layout.label(text=text, icon="COLOR")
row = self.layout.row(align=True)
if props.is_editing_item_shape_aspect:
row.prop(props, "representation_item_shape_aspect", icon="SHAPEKEY_DATA", text="")
@@ -59,6 +59,9 @@ class GeoreferenceDecorator:
batch.draw(shader)
def draw_text(self, context):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.calculate_angles(context)
props = context.scene.BIMGeoreferenceProperties
e, n, h = [round(float(o), 7) for o in props.model_origin.split(",")]
@@ -164,6 +167,9 @@ class GeoreferenceDecorator:
blf.draw(self.font_id, line)
def draw_geometry(self, context):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.calculate_angles(context)
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -269,6 +269,7 @@ class RadianceExporterProperties(PropertyGroup):
should_load_from_memory: BoolProperty(
name="Load from Memory",
default=False,
description="Use IFC file currently loaded in Bonsai",
)
radiance_resolution_x: IntProperty(
+4 -19
View File
@@ -33,7 +33,6 @@ from . import (
window,
opening,
mep,
pie,
workspace,
profile,
sverchok_modifier,
@@ -119,6 +118,7 @@ classes = (
mep.RegenerateDistributionElement,
prop.SnapMousePoint,
prop.PolylinePoint,
prop.PolylineMeasurement,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -138,17 +138,12 @@ classes = (
ui.BIM_MT_model,
ui.BIM_PT_Grids,
grid.BIM_OT_add_object,
stair.BIM_OT_add_object,
stair.BIM_OT_add_clever_stair,
stair.BIM_OT_add_stair,
stair.AddStair,
stair.CancelEditingStair,
stair.FinishEditingStair,
stair.EnableEditingStair,
stair.RemoveStair,
pie.OpenPieClass,
pie.PieAddOpening,
pie.VIEW3D_MT_PIE_bim,
pie.VIEW3D_MT_PIE_bim_class,
sverchok_modifier.CreateNewSverchokGraph,
sverchok_modifier.UpdateDataFromSverchok,
sverchok_modifier.DeleteSverchokGraph,
@@ -220,13 +215,8 @@ def register():
bpy.types.VIEW3D_MT_mesh_add.append(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
kmi = km.keymap_items.new("wm.call_menu_pie", "E", "PRESS", shift=True)
kmi.properties["name"] = "VIEW3D_MT_PIE_bim"
addon_keymaps.append((km, kmi))
workspace.load_custom_icons()
def unregister():
if not bpy.app.background:
@@ -252,9 +242,4 @@ def unregister():
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_mesh_add.remove(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
workspace.unload_custom_icons()
+222 -36
View File
@@ -21,11 +21,14 @@ import gpu
import gpu_extras
import bmesh
import bonsai.tool as tool
from math import sin, cos, radians
from math import sin, cos, radians, degrees, atan2, acos
from bpy.types import SpaceView3D
import math
from bpy_extras import view3d_utils
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
def transparent_color(color, alpha=0.1):
@@ -295,7 +298,7 @@ class ProfileDecorator:
return points, listEdg
class WallPolylineDecorator:
class PolylineDecorator:
is_installed = False
handlers = []
mouse_pos = None
@@ -303,13 +306,15 @@ class WallPolylineDecorator:
input_type = None
angle_snap_mat = None
angle_snap_loc = None
use_default_container = False
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_panel, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@@ -336,14 +341,29 @@ class WallPolylineDecorator:
cls.axis_start = start
cls.axis_end = end
@classmethod
def set_axis_rectangle(cls, corners):
cls.axis_rectangle = [*corners]
@classmethod
def set_use_default_container(cls, value=False):
cls.use_default_container = value
@classmethod
def set_plane(cls, plane_origin, plane_normal):
cls.plane_origin = plane_origin
cls.plane_normal = plane_normal
@classmethod
def calculate_distance_and_angle(cls, context, is_input_on):
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
last_point_data = polyline_data[len(polyline_data) - 1]
except:
last_point_data = None
second_to_last_point_data = None
default_container_elevation = 0
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
@@ -358,22 +378,32 @@ class WallPolylineDecorator:
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
)
else:
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
if cls.use_default_container:
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
else:
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
second_to_last_point = None
if len(polyline_data) > 1:
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
second_to_last_point = Vector(
(second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
)
else:
# Creates a fake "second to last" point away from the first point but in the same x axis
# this allows to calculate the angle relative to x axis when there is only one point
second_to_last_point = Vector((last_point.x - 1000, last_point.y, last_point.z))
distance = (snap_vector - last_point).length
x_axis_edge = (
Vector((0, 0)),
Vector((1, 0)),
)
current_axis = (
Vector((last_point.x, last_point.y)),
Vector((snap_vector.x, snap_vector.y)),
)
if distance > 0:
angle = tool.Cad.angle_edges(x_axis_edge, current_axis, degrees=True, signed=True)
print(snap_vector, last_point, second_to_last_point)
angle = tool.Cad.angle_3_vectors(snap_vector, last_point, second_to_last_point, degrees=True)
print("A", angle)
if cls.input_panel:
cls.input_panel["X"] = str(round(snap_vector.x, 4))
cls.input_panel["Y"] = str(round(snap_vector.y, 4))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(snap_vector.z, 4))
cls.input_panel["D"] = str(round(distance, 4))
cls.input_panel["A"] = str(round(angle, 4))
@@ -382,24 +412,95 @@ class WallPolylineDecorator:
return cls.input_panel
@classmethod
def calculate_x_and_y(cls, context):
def calculate_area(cls, context):
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
except:
return cls.input_panel
if len(polyline_data) < 3:
return cls.input_panel
points = []
for data in polyline_data:
points.append(Vector((data.x, data.y, data.z)))
if points[0] == points[-1]:
points = points[1:]
# TODO move this to CAD
# Calculate the normal vector of the plane formed by the first three vertices
v1, v2, v3 = points[:3]
normal = (v2 - v1).cross(v3 - v1).normalized()
# Check if all points are coplanar
is_coplanar = True
tolerance = 1e-6 # Adjust this value as needed
for v in points:
if abs((v - v1).dot(normal)) > tolerance:
is_coplanar = False
if is_coplanar:
area = 0
for i in range(len(points)):
j = (i + 1) % len(points)
area += points[i].cross(points[j]).dot(normal)
area = abs(area) / 2
else:
area = 0
if "AREA" in list(cls.input_panel.keys()):
cls.input_panel["AREA"] = str(round(area, 4))
return cls.input_panel
@classmethod
def calculate_x_y_and_z(cls, context):
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
last_point_data = polyline_data[len(polyline_data) - 1]
except:
return
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
second_to_last_point = None
if len(polyline_data) > 1:
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
second_to_last_point = Vector(
(second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
)
else:
# Creates a fake "second to last" point away from the first point but in the same x axis
# this allows to calculate the angle relative to x axis when there is only one point
second_to_last_point = Vector((last_point.x - 10, last_point.y, last_point.z))
distance = float(cls.input_panel["D"])
if distance < 0 or distance > 0:
angle_degrees = float(cls.input_panel["A"])
angle_radians = radians(360 - angle_degrees) # Substracting from 360 to make it clockwise
x = last_point[0] + distance * cos(angle_radians)
y = last_point[1] + distance * sin(angle_radians)
angle_rad = radians(180 - float(cls.input_panel["A"]))
ref_vec = second_to_last_point - last_point
dir_vec = last_point - snap_vector
rot_axis = ref_vec.cross(dir_vec)
rot_axis.normalize()
rot_axis = Vector((abs(rot_axis.x), abs(rot_axis.y), abs(rot_axis.z)))
rot_mat = Matrix.Rotation(angle_rad, 3, rot_axis)
ref_vec.normalize()
coords = ((ref_vec @ rot_mat) * distance) + last_point
x = coords[0]
y = coords[1]
z = coords[2]
if cls.input_panel:
cls.input_panel["X"] = str(round(x, 4))
cls.input_panel["Y"] = str(round(y, 4))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(z, 4))
return cls.input_panel
@@ -411,8 +512,17 @@ class WallPolylineDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text(self, context):
texts = ["X coord:", "Y coord:", "Distance:", "Angle:"]
def draw_input_panel(self, context):
texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"}
unit_system = tool.Drawing.get_unit_system()
if unit_system == "IMPERIAL":
precision = context.scene.DocProperties.imperial_precision
factor = 3.28084
else:
precision = None
factor = 1
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
blf.size(self.font_id, 12)
@@ -422,14 +532,67 @@ class WallPolylineDecorator:
color_highlight = self.addon_prefs.decorator_color_special
offset = 20
new_line = 20
keys = list(self.input_panel.keys())
for i, text in enumerate(texts):
if keys[i] == self.input_type:
for i, (key, value) in enumerate(self.input_panel.items()):
if key != "A" and key != self.input_type:
value = float(value)
if context.scene.unit_settings.length_unit == 'MILLIMETERS':
value = value * 1000
formatted_value = format_distance(value*factor, precision=precision, suppress_zero_inches=True, in_unit_length=True)
else:
formatted_value = value
if key not in list(texts.keys()):
continue
if key == self.input_type:
blf.color(self.font_id, *color_highlight)
else:
blf.color(self.font_id, *color)
blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] + offset - (new_line * i), 0)
blf.draw(self.font_id, text + self.input_panel[keys[i]])
blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] - (new_line * i), 0)
blf.draw(self.font_id, texts[key] + formatted_value)
def draw_measurements(self, context):
region = context.region
rv3d = region.data
measurement_prop = context.scene.BIMModelProperties.polyline_measurement
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
blf.size(self.font_id, 12)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color)
for i in range(len(measurement_prop)):
if i == 0:
continue
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i-1].position)) / 2
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
unit_system = tool.Drawing.get_unit_system()
if unit_system == "IMPERIAL":
precision = context.scene.DocProperties.imperial_precision
factor = 3.28084
else:
precision = None
factor = 1
value = measurement_prop[i].dim
value = float(value)
if context.scene.unit_settings.length_unit == 'MILLIMETERS':
value = value * 1000
formatted_value = format_distance(value*factor, precision=precision, suppress_zero_inches=True, in_unit_length=True)
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + formatted_value)
pos_angle = measurement_prop[i-1].position
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
blf.draw(self.font_id, "a: " + measurement_prop[i].angle)
def __call__(self, context):
@@ -452,24 +615,34 @@ class WallPolylineDecorator:
gpu.state.point_size_set(10)
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
# Point related to the mouse
mouse_point = [Vector((snap_prop.x, snap_prop.y, snap_prop.z))]
try:
snap_ref = context.scene.BIMModelProperties.snap_mouse_ref[0]
ref_point = [Vector((snap_ref.x, snap_ref.y, snap_ref.z))]
except:
ref_point = None
if snap_prop.snap_type in ["Face", "Plane"]:
self.draw_batch("POINTS", mouse_point, decorator_color_unselected)
else:
self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0))
# When a point is above the plane it projects the point
# to the plane and creates a line
if ref_point:
self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0))
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
projection_point = []
if snap_prop.snap_type != "Plane" and snap_prop.z != 0:
self.line_shader.uniform_float("lineWidth", 1.0)
projection_point = [Vector((snap_prop.x, snap_prop.y, default_container_elevation))]
self.draw_batch("POINTS", projection_point, decorator_color_unselected)
edges = [[0, 1]]
self.draw_batch("LINES", mouse_point + projection_point, (1.0, 0.6, 0.0, 1.0), edges)
if self.use_default_container:
# When a point is above the plane it projects the point
# to the plane and creates a line
if snap_prop.snap_type != "Plane" and snap_prop.z != 0:
self.line_shader.uniform_float("lineWidth", 1.0)
projection_point = [Vector((snap_prop.x, snap_prop.y, default_container_elevation))]
self.draw_batch("POINTS", projection_point, decorator_color_unselected)
edges = [[0, 1]]
self.draw_batch("LINES", mouse_point + projection_point, (1.0, 0.6, 0.0, 1.0), edges)
# Polyline with selected points
polyline_data = context.scene.BIMModelProperties.polyline_point
@@ -490,7 +663,7 @@ class WallPolylineDecorator:
# Line between last polyline point and mouse
edges = [[0, 1]]
if polyline_points:
if snap_prop.snap_type != "Plane" and snap_prop.z != 0 and projection_point:
if snap_prop.snap_type != "Plane" and projection_point:
self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges)
else:
self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges)
@@ -499,3 +672,16 @@ class WallPolylineDecorator:
if snap_prop.snap_type == "Axis":
self.line_shader.uniform_float("lineWidth", 0.75)
self.draw_batch("LINES", [self.axis_start, self.axis_end], decorator_color_unselected, [(0, 1)])
try:
self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)])
except:
pass
# Area highlight
if "AREA" in list(self.input_panel.keys()):
if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0:
edges = []
for i in range(1, len(polyline_points) - 1):
edges.append((0, i, i + 1))
self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges)
@@ -494,6 +494,10 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Door"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
+1 -1
View File
@@ -82,7 +82,7 @@ class BIM_OT_add_object(Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
add_object(self, context)
-88
View File
@@ -1,88 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai 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.
#
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
class OpenPieClass(bpy.types.Operator):
bl_idname = "bim.open_pie_class"
bl_label = "Open Pie Class"
bl_description = "Assign the IFC Class to the selected objects"
@classmethod
def poll(cls, context):
if not context.active_object and not context.selected_objects:
cls.poll_message_set("No object selected.")
return False
return True
def execute(self, context):
bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_bim_class")
return {"FINISHED"}
class PieAddOpening(bpy.types.Operator):
bl_idname = "bim.pie_add_opening"
bl_label = "Add Opening"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
if len(context.selected_objects) == 2:
opening_name = None
obj_name = None
for obj in context.selected_objects:
if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id:
opening_name = obj.name
elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id:
opening_name = obj.children[0].name
else:
obj_name = obj.name
bpy.ops.bim.add_opening(obj=obj_name, opening=opening_name)
return {"FINISHED"}
class VIEW3D_MT_PIE_bim(bpy.types.Menu):
bl_label = "Geometry"
def draw(self, context):
pie = self.layout.menu_pie()
pie.operator("bim.edit_object_placement")
pie.operator("bim.update_representation").ifc_representation_class = ""
pie.operator("bim.pie_add_opening")
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")
class VIEW3D_MT_PIE_bim_class(bpy.types.Menu):
bl_label = "Class"
def draw(self, context):
pie = self.layout.menu_pie()
pie.operator("bim.assign_class", text="IfcWall").ifc_class = "IfcWall"
pie.operator("bim.assign_class", text="IfcSlab").ifc_class = "IfcSlab"
pie.operator("bim.assign_class", text="IfcStair").ifc_class = "IfcStair"
pie.operator("bim.assign_class", text="IfcDoor").ifc_class = "IfcDoor"
pie.operator("bim.assign_class", text="IfcWindow").ifc_class = "IfcWindow"
pie.operator("bim.assign_class", text="IfcColumn").ifc_class = "IfcColumn"
pie.operator("bim.assign_class", text="IfcBeam").ifc_class = "IfcBeam"
@@ -144,8 +144,10 @@ class AddConstrTypeInstance(bpy.types.Operator):
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
props.relating_type_id = str(self.relating_type_id)
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
@@ -56,10 +56,13 @@ class DumbProfileGenerator:
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
props = bpy.context.scene.BIMModelProperties
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.depth = props.extrusion_depth
self.rotation = 0
self.location = Vector((0, 0, 0))
@@ -111,6 +111,10 @@ class PolylinePoint(PropertyGroup):
y: bpy.props.FloatProperty(name="Y")
z: bpy.props.FloatProperty(name="Z")
class PolylineMeasurement(PropertyGroup):
dim: bpy.props.StringProperty(name="Dimension")
angle: bpy.props.StringProperty(name="Angle")
position: bpy.props.FloatVectorProperty(name="Decorator Position", size=3)
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
@@ -204,7 +208,9 @@ class BIMModelProperties(PropertyGroup):
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
snap_mouse_point: bpy.props.CollectionProperty(type=SnapMousePoint)
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
polyline_measurement: bpy.props.CollectionProperty(type=PolylineMeasurement)
class BIMArrayProperties(PropertyGroup):
@@ -283,6 +283,10 @@ class BIM_OT_add_railing(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Railing"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
@@ -499,6 +499,10 @@ class BIM_OT_add_roof(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Roof"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
@@ -135,10 +135,13 @@ class DumbSlabGenerator:
)
props = bpy.context.scene.BIMModelProperties
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.depth = sum(thicknesses) * unit_scale
self.width = 3
self.length = 3
+7 -51
View File
@@ -33,54 +33,6 @@ from bpy.props import FloatProperty, IntProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
def add_dumb_stair_object(self, context):
verts = [
Vector((0, 0, 0)),
Vector((0, self.tread_length, 0)),
Vector((self.width, self.tread_length, 0)),
Vector((self.width, 0, 0)),
]
edges = []
faces = [[0, 1, 2, 3]]
mesh = bpy.data.meshes.new(name="Dumb Stair")
mesh.from_pydata(verts, edges, faces)
obj = object_data_add(context, mesh, operator=self)
modifier = obj.modifiers.new("Stair Width", "SOLIDIFY")
modifier.use_even_offset = True
modifier.offset = 1
modifier.thickness = self.tread_depth
modifier = obj.modifiers.new("Stair Treads", "ARRAY")
modifier.relative_offset_displace[0] = 0
modifier.relative_offset_displace[1] = 1
modifier.use_constant_offset = True
modifier.constant_offset_displace[0] = 0
modifier.constant_offset_displace[1] = 0
modifier.constant_offset_displace[2] = self.height / self.number_of_treads
modifier.count = self.number_of_treads
self.riser_height = self.height / self.number_of_treads
self.length = self.number_of_treads * self.tread_length
obj.name = "Stair"
class BIM_OT_add_object(Operator, AddObjectHelper):
bl_idname = "mesh.add_stair"
bl_label = "Dumb Stair"
bl_options = {"REGISTER", "UNDO"}
width: FloatProperty(name="Width", default=1.1)
height: FloatProperty(name="Height", default=1)
tread_depth: FloatProperty(name="Tread Depth", default=0.2)
number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6, soft_min=1)
tread_length: FloatProperty(name="Tread Length (Going)", default=0.25)
riser_height: FloatProperty(name="*Calculated* Riser Height")
length: FloatProperty(name="*Calculated* Length")
def execute(self, context):
add_dumb_stair_object(self, context)
return {"FINISHED"}
def regenerate_stair_mesh(context):
obj = context.active_object
props_kwargs = obj.BIMStairProperties.get_props_kwargs()
@@ -198,11 +150,15 @@ def update_ifc_stair_props(obj):
tool.Drawing.setup_annotation_object(annotation_obj, "STAIR_ARROW", stair_obj)
class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_clever_stair"
bl_label = "Clever Stair"
class BIM_OT_add_stair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_stair"
bl_label = "Stair"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
+8 -8
View File
@@ -663,11 +663,11 @@ def add_menu(self, context):
def add_mesh_object_menu(self, context):
self.layout.separator()
self.layout.operator("mesh.add_grid", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("mesh.add_stair", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("mesh.add_clever_stair", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("mesh.add_window", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("mesh.add_door", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("mesh.add_railing", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("mesh.add_roof", icon_value=bonsai.bim.icons["IFC"].icon_id)
if context.mode == "OBJECT":
self.layout.separator()
self.layout.operator("mesh.add_grid", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("mesh.add_stair", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Stair (Untyped)")
self.layout.operator("mesh.add_window", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Window (Untyped)")
self.layout.operator("mesh.add_door", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Door (Untyped)")
self.layout.operator("mesh.add_railing", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Railing (Untyped)")
self.layout.operator("mesh.add_roof", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Roof (Untyped)")
+101 -245
View File
@@ -37,7 +37,7 @@ from bonsai.bim.ifc import IfcStore
from math import pi, sin, cos, degrees
from mathutils import Vector, Matrix
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import WallPolylineDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator
from typing import Optional
from lark import Lark, Transformer
@@ -285,9 +285,6 @@ class DrawPolylineWall(bpy.types.Operator):
bl_label = "Draw Polyline Wall"
bl_options = {"REGISTER", "UNDO"}
objs_bvhs = []
viewport_box = []
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
@@ -297,203 +294,56 @@ class DrawPolylineWall(bpy.types.Operator):
self.action_count = 0
self.visible_objs = []
self.objs_2d_bbox = []
self.number_options = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "+", "-", "*", "-", "/"}
self.number_options = {
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
" ",
".",
"+",
"-",
"*",
"/",
"'",
'"',
"=",
}
self.number_input = []
self.number_output = ""
self.number_is_negative = False
self.is_input_on = False
self.input_options = ["X", "Y", "D", "A"]
self.input_options = ["D", "A", "X", "Y"]
self.input_type = "OFF"
self.input_value_xy = [None, None]
self.input_panel = {"X": "", "Y": "", "D": "", "A": ""}
self.input_panel = {"D": "", "A": "", "X": "", "Y": ""}
self.snap_angle = None
def snaping_movement(self, context, event):
region = context.region
rv3d = context.region_data
self.mouse_pos = event.mouse_region_x, event.mouse_region_y
offset = 5
mouse_offset = (
(-offset, offset),
(0, offset),
(offset, offset),
(-offset, 0),
(0, 0),
(offset, 0),
(-offset, -offset),
(0, -offset),
(offset, -offset),
)
# Plane to intersect. Default Container
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
plane_origin = Vector((0, 0, default_container_elevation))
plane_normal = Vector((0, 0, 1))
def cast_rays_and_get_best_object():
best_length_squared = 1.0
best_obj = None
best_hit = None
best_face_index = None
objs_to_raycast = []
for obj, bbox_2d in self.objs_2d_bbox:
if obj.type == "MESH" and bbox_2d:
if tool.Raycast.in_view_2d_bounding_box(self.mouse_pos, bbox_2d):
objs_to_raycast.append(obj)
for obj in objs_to_raycast:
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
if hit is None:
# Tried original mouse position. Now it will try the offsets.
original_mouse_pos = self.mouse_pos
for value in mouse_offset:
self.mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value))
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
if hit:
break
self.mouse_pos = original_mouse_pos
if hit is not None:
hit_world = obj.original.matrix_world @ hit
length_squared = (hit_world - ray_origin).length_squared
if best_obj is None or length_squared < best_length_squared:
best_length_squared = length_squared
best_obj = obj
best_hit = hit_world
best_face_index = face_index
if best_obj is not None:
return best_obj, best_hit, best_face_index
else:
return None, None, None
snap_threshold = 0.3
ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event)
obj, hit, face_index = cast_rays_and_get_best_object()
intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal)
# Locks snap into an angle axis
if event.shift:
rot_intersection, _, axis_start, axis_end = tool.Snap.snap_on_axis(intersection, self.snap_angle)
else:
self.snap_angle = None
rot_intersection, self.snap_angle, _, _ = tool.Snap.snap_on_axis(intersection)
if obj is not None:
snap_points = tool.Snap.get_snap_points_on_raycasted_obj(obj, face_index)
snap_point = tool.Snap.select_snap_point(snap_points, hit, snap_threshold)
if snap_point:
# Creates a mixed snap point between the locked axis and
# the object snap
# TODO Use ALT key to give the user the option to choose between the two results.
# TODO Create decorator for this
try:
snap_point_vector = Vector((snap_point[0].x, snap_point[0].y, snap_point[0].z))
snap_point_axis_1 = (
Vector((snap_point[0].x + 1000, snap_point[0].y, default_container_elevation)),
Vector((snap_point[0].x - 1000, snap_point[0].y, default_container_elevation)),
)
snap_point_axis_2 = (
Vector((snap_point[0].x, snap_point[0].y + 1000, default_container_elevation)),
Vector((snap_point[0].x, snap_point[0].y - 1000, default_container_elevation)),
)
snap_angle_axis = (axis_start, axis_end)
result_1 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_1)
result_1 = Vector((result_1[0].x, result_1[0].y, default_container_elevation))
distance_1 = (result_1 - snap_point_vector).length
result_2 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_2)
result_2 = Vector((result_2[0].x, result_2[0].y, default_container_elevation))
distance_2 = (result_2 - snap_point_vector).length
if distance_1 < distance_2:
best_result = result_1
else:
best_result = result_2
tool.Snap.update_snaping_point(best_result, "Axis")
except Exception as e:
tool.Snap.update_snaping_point(snap_point[0], snap_point[1])
else:
tool.Snap.update_snaping_point(hit, "Face")
else:
snap_points = tool.Snap.get_snap_points_on_polyline()
snap_point = tool.Snap.select_snap_point(snap_points, intersection, snap_threshold)
if snap_point:
tool.Snap.update_snaping_point(snap_point[0], snap_point[1])
elif rot_intersection:
tool.Snap.update_snaping_point(rot_intersection, "Axis")
else:
tool.Snap.update_snaping_point(intersection, "Plane")
def validate_input(self, input_number):
grammar = """
start: dim expr?
dim: NUMBER
expr: (ADD | SUB | MUL | DIV) NUMBER
NUMBER: /-?\\d+(?:\\.\\d+)?/
ADD: "+"
SUB: "-"
MUL: "*"
DIV: "/"
%ignore " "
"""
class InputTransform(Transformer):
def dim(self, args):
return float(args[0])
def expr(self, args):
op = args[0]
value = float(args[1])
if op == "+":
return lambda x: x + value
elif op == "-":
return lambda x: x - value
elif op == "*":
return lambda x: x * value
elif op == "/":
return lambda x: x / value
def start(self, args):
dimension = args[0]
if len(args) > 1:
expression = args[1]
return expression(dimension)
else:
return dimension
try:
parser = Lark(grammar, parser="lalr", transformer=InputTransform())
result = parser.parse(input_number)
return True, str(result)
except:
self.report({"WARNING"}, "The number typed is not valid.")
return False, "0"
self.snapping_points = []
def recalculate_inputs(self, context):
if self.number_input:
is_valid, self.number_output = self.validate_input(self.number_output)
is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type)
self.input_panel[self.input_type] = self.number_output
if is_valid:
if not is_valid:
self.report({"WARNING"}, "The number typed is not valid.")
return is_valid
else:
if self.input_type in {"X", "Y"}:
self.input_panel = WallPolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
elif self.input_type in {"D", "A"}:
self.input_panel = WallPolylineDecorator.calculate_x_and_y(context)
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
self.input_panel[self.input_type] = self.number_output
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
return is_valid
# TODO This is creating a hack in generate function from DumbWallGenerator
# Come up with a better solution
@@ -504,32 +354,32 @@ class DrawPolylineWall(bpy.types.Operator):
if not relating_type_id:
return {"FINISHED"}
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
walls, is_polyline_closed = DumbWallGenerator(relating_type).generate(True)
print(is_polyline_closed)
if walls:
if is_polyline_closed:
for wall1, wall2 in zip(walls, walls[1:] + [walls[0]]):
print(wall1["obj"], wall2["obj"])
DumbWallJoiner().join_V(wall1["obj"], wall2["obj"])
else:
for wall1, wall2 in zip(walls[:-1], walls[1:]):
print(wall1["obj"], wall2["obj"])
DumbWallJoiner().join_V(wall1["obj"], wall2["obj"])
def modal(self, context, event):
if not self.is_input_on:
if event.type == "MOUSEMOVE":
if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
self.mousemove_count += 1
self.is_input_on = False
self.input_type = "OFF"
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
else:
self.mousemove_count = 0
@@ -540,41 +390,41 @@ class DrawPolylineWall(bpy.types.Operator):
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
if self.mousemove_count > 3:
self.snaping_movement(context, event)
WallPolylineDecorator.set_mouse_position(event)
self.input_panel = WallPolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
return {"RUNNING_MODAL"}
if event.value == "RELEASE" and event.type == "BACK_SPACE":
tool.Snap.remove_last_polyline_point()
tool.Blender.update_viewport()
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
tool.Snap.insert_polyline_point()
tool.Snap.insert_polyline_point(self.input_panel)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "C":
tool.Snap.close_polyline()
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on:
if event.value == "RELEASE" and event.type == "BACK_SPACE":
tool.Snap.remove_last_polyline_point()
tool.Blender.update_viewport()
if self.is_input_on and event.value == "PRESS" and event.type == "TAB":
self.recalculate_inputs(context)
index = self.input_options.index(self.input_type)
size = len(self.input_options)
self.input_type = self.input_options[((index + 1) % size)]
self.number_input = []
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB":
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "X"
self.input_type = "D"
self.number_input = []
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on and event.ascii in self.number_options:
@@ -582,7 +432,7 @@ class DrawPolylineWall(bpy.types.Operator):
self.is_input_on = True
self.input_type = "D"
self.number_input = []
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "RELEASE" and event.type in self.input_options:
@@ -590,13 +440,12 @@ class DrawPolylineWall(bpy.types.Operator):
self.is_input_on = True
self.input_type = event.type
self.number_input = []
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.input_panel[self.input_type] = ""
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if self.input_type in self.input_options:
if (event.ascii in self.number_options) or (
event.value == "RELEASE" and event.type in {"BACK_SPACE", "MINUS", "NUMPAD_MINUS"}
):
if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"):
if event.type == "BACK_SPACE":
if len(self.number_input) <= 1:
self.number_input = []
@@ -604,7 +453,7 @@ class DrawPolylineWall(bpy.types.Operator):
self.number_input = self.number_input[:-1]
self.number_output = "".join(self.number_input)
self.input_panel[self.input_type] = self.number_output
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
else:
self.number_input.append(event.ascii)
@@ -612,45 +461,43 @@ class DrawPolylineWall(bpy.types.Operator):
if self.number_input:
self.input_panel[self.input_type] = self.number_output
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER"}:
if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
self.create_walls_from_polyline(context)
WallPolylineDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
return {"FINISHED"}
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER"}:
if self.input_type in self.input_options:
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "OFF"
self.number_input = []
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
else:
tool.Snap.insert_polyline_point(float(self.input_panel["X"]), float(self.input_panel["Y"]))
self.is_input_on = False
self.input_type = "OFF"
self.number_input = []
self.number_output = ""
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
is_valid = self.recalculate_inputs(context)
if is_valid:
tool.Snap.insert_polyline_point(self.input_panel)
self.is_input_on = False
self.input_type = "OFF"
self.number_input = []
self.number_output = ""
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "M":
self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
tool.Blender.update_viewport()
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"}
if self.is_input_on:
if event.value == "RELEASE" and event.type in {"RIGHTMOUSE", "ESC"}:
if event.value == "RELEASE" and event.type in {"ESC"}:
self.is_input_on = False
self.input_type = "OFF"
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
else:
if event.value == "RELEASE" and event.type in {"RIGHTMOUSE", "ESC"}:
WallPolylineDecorator.uninstall()
if event.value == "RELEASE" and event.type in {"ESC"}:
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
return {"CANCELLED"}
@@ -659,14 +506,18 @@ class DrawPolylineWall(bpy.types.Operator):
def invoke(self, context, event):
if context.space_data.type == "VIEW_3D":
WallPolylineDecorator.install(context)
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
PolylineDecorator.install(context)
tool.Snap.set_use_default_container(True)
PolylineDecorator.set_use_default_container(True)
tool.Snap.set_snap_plane_method("XY")
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
self.snaping_movement(context, event)
WallPolylineDecorator.set_mouse_position(event)
self.input_panel = WallPolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
@@ -797,10 +648,13 @@ class DumbWallGenerator:
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
props = bpy.context.scene.BIMModelProperties
self.container = None
self.container_obj = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.width = self.layers["thickness"]
self.height = props.extrusion_depth
self.length = props.length
@@ -846,7 +700,7 @@ class DumbWallGenerator:
for stroke in layer.active_frame.strokes:
if len(stroke.points) == 1:
continue
data = self.create_wall_from_2_points((stroke.points[0].co, stroke.points[-1].co))
data = self.create_wall_from_2_points((stroke.points[0].co, stroke.points[-1].co), round=True)
if data:
strokes.append(data)
objs.append(data["obj"])
@@ -871,19 +725,21 @@ class DumbWallGenerator:
bpy.context.scene.grease_pencil.layers.remove(layer)
return objs
def create_wall_from_2_points(self, coords):
def create_wall_from_2_points(self, coords, round=False):
direction = coords[1] - coords[0]
length = direction.length
if length < 0.1:
return
data = {"coords": coords}
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
self.length = length
self.rotation = math.atan2(direction[1], direction[0])
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
if round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_wall()
return data
@@ -387,6 +387,10 @@ class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Window"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
def _execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
+384 -92
View File
@@ -18,6 +18,7 @@
import os
import bpy
import bpy.utils.previews
import bonsai.tool as tool
import bonsai.core.model as core
from bonsai.bim.module.model.wall import DumbWallJoiner
@@ -27,11 +28,29 @@ from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.system.data import PortData
from bonsai.bim.module.model.prop import get_ifc_class
custom_icon_previews = None
def load_custom_icons():
global custom_icon_previews
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
custom_icon_previews = bpy.utils.previews.new()
for entry in os.scandir(icons_dir):
if entry.name.endswith(".png"):
name = os.path.splitext(entry.name)[0]
custom_icon_previews.load(name.upper(), entry.path, "IMAGE")
def unload_custom_icons():
global custom_icon_previews
if custom_icon_previews:
bpy.utils.previews.remove(custom_icon_previews)
custom_icon_previews = None
class BimTool(WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.bim_tool"
bl_label = "BIM Tool"
bl_description = "Create and edit elements by construction class"
@@ -220,7 +239,7 @@ class CableTool(BimTool):
BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey_operator(layout, text, hotkey, description):
def add_layout_hotkey_operator(layout, text, hotkey, description, ui_context=""):
modifiers = {
"A": "EVENT_ALT",
"C": "EVENT_CTRL",
@@ -228,11 +247,19 @@ def add_layout_hotkey_operator(layout, text, hotkey, description):
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
try:
custom_icon = custom_icon_previews[text.upper().replace(" ", "_")].icon_id
except KeyError:
custom_icon = custom_icon_previews["IFC"].icon_id
if ui_context == "TOOL_HEADER":
op = layout.operator("bim.hotkey", text="", icon_value=custom_icon)
else:
row = layout.row(align=True)
op = row.operator("bim.hotkey", text=text, icon_value=custom_icon)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op
@@ -260,14 +287,19 @@ class BimToolUI:
AuthoringData.load(ifc_element_type)
if context.region.type == "TOOL_HEADER":
cls.draw_header_interface()
elif context.region.type in ("UI", "WINDOW"):
# same interface for both n-panel sidebar and object properties
cls.draw_basic_bim_tool_interface()
cls.draw_container(context)
cls.draw_type_selection_interface(context)
if context.active_object and context.selected_objects:
cls.draw_edit_object_header_interface(context)
if context.active_object and context.selected_objects:
cls.draw_edit_object_interface(context)
elif not context.selected_objects:
elif context.region.type in ("UI", "WINDOW"):
cls.draw_container(context)
cls.draw_thumbnail()
cls.draw_type_selection_interface(context)
if context.active_object and context.selected_objects:
cls.draw_edit_object_panel_interface(context)
if not context.selected_objects:
cls.draw_create_object_interface()
@classmethod
@@ -285,10 +317,10 @@ class BimToolUI:
row.prop(data=cls.props, property="length", text="Length")
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle")
elif cls.props.ifc_class in ("IfcSlabType", "IfcRampType", "IfcRoofType"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle")
elif cls.props.ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="cardinal_point", text="Axis")
@@ -318,47 +350,77 @@ class BimToolUI:
row.prop(data=cls.props, property="rl_mode", text="RL")
@classmethod
def draw_edit_object_interface(cls, context):
def draw_edit_object_panel_interface(cls, context):
ui_context = str(context.region.type)
if AuthoringData.data["active_material_usage"] == "LAYER2":
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="extrusion_depth", text="Height")
row.prop(data=cls.props, property="extrusion_depth", text="Height:")
op = row.operator("bim.change_extrusion_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="length", text="Length")
row.prop(data=cls.props, property="length", text="Length:")
op = row.operator("bim.change_layer_length", icon="FILE_REFRESH", text="")
op.length = cls.props.length
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle:")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
cls.layout.separator()
add_layout_hotkey_operator(
cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__ + "\n⇧ G", ui_context
)
cls.layout.separator()
add_layout_hotkey_operator(
cls.layout,
"Extend",
"S_E",
"Extends/reduces element to 3D cursor\n⇧ E",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Butt",
"S_T",
"Intersects two non-parallel elements to a butt corner junction\n⇧ T",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Mitre",
"S_Y",
"Join two intersecting walls using a mitre joint.\nOther selected wall is connected to the active",
"Intersects two non-parallel elements to a mitred corner junction\n⇧ Y",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
row.operator("bim.unjoin_walls", icon="X", text="")
row = cls.layout.row(align=True)
row.operator(
"bim.unjoin_walls", text="Unjoin Walls", icon_value=custom_icon_previews["UNJOIN_WALLS"].icon_id
)
cls.layout.separator()
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__ + "\n⇧ M", ui_context)
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__ + "\n⇧ K", ui_context)
add_layout_hotkey_operator(
cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__ + "\n⇧ R", ui_context
)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__ + "\n⇧ F", ui_context)
# row.operator("bim.unjoin_walls", icon="X", text="")
elif AuthoringData.data["active_material_usage"] == "LAYER3":
if len(context.selected_objects) == 1:
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif "LAYER2" in AuthoringData.data["selected_material_usages"]:
add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "", ui_context)
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
@@ -376,8 +438,14 @@ class BimToolUI:
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
add_layout_hotkey_operator(
cls.layout,
"Extend",
"S_E",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
@@ -386,18 +454,37 @@ class BimToolUI:
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "")
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_obstruction")
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "", ui_context)
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_obstruction")
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
add_layout_hotkey_operator(
cls.layout,
"Edit Axis",
"A_E",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Butt",
"S_T",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Mitre",
"S_Y",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
add_layout_hotkey_operator(
cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__, ui_context
)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif (
@@ -422,15 +509,15 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL")
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "")
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "", ui_context)
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
if not tool.Model.is_parametric_window_active() and not tool.Model.is_parametric_door_active():
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif AuthoringData.data["active_class"] in ("IfcSpace",):
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__, ui_context)
elif tool.Model.is_parametric_roof_active() and not context.active_object.BIMRoofProperties.is_editing_path:
row = cls.layout.row(align=True)
@@ -439,25 +526,25 @@ class BimToolUI:
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
add_layout_hotkey_operator(
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__, ui_context
)
cls.layout.operator("bim.mep_connect_elements")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if len(context.selected_objects) > 1:
row.operator("bim.add_opening", text="Apply Void")
row.operator("bim.add_opening", text="Apply Void", icon_value=custom_icon_previews["VOID"].icon_id)
else:
row.operator("bim.add_potential_opening", text="Add Void")
row.operator(
"bim.add_potential_opening", text="Add Void", icon_value=custom_icon_previews["ADD_VOID"].icon_id
)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
row = cls.layout.row(align=True)
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
@@ -469,51 +556,264 @@ class BimToolUI:
row.operator("bim.clone_opening", text="Clone Opening")
cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "")
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
add_layout_hotkey_operator(cls.layout, "Exterior", "S_X", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Centerline", "S_C", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Interior", "S_V", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__, ui_context)
cls.layout.row(align=True).label(text="Mode")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings", ui_context)
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition", ui_context)
cls.layout.row(align=True).label(text="Aggregation")
add_layout_hotkey_operator(cls.layout, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__)
add_layout_hotkey_operator(cls.layout, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__)
cls.layout.separator()
add_layout_hotkey_operator(cls.layout, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__, ui_context)
add_layout_hotkey_operator(
cls.layout, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__
cls.layout, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__, ui_context
)
cls.layout.row(align=True).label(text="Qto")
add_layout_hotkey_operator(
cls.layout, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__, ui_context
)
@classmethod
def draw_header_interface(cls):
cls.draw_type_selection_interface()
if AuthoringData.data["ifc_classes"]:
def draw_edit_object_header_interface(cls, context):
ui_context = str(context.region.type)
if AuthoringData.data["active_material_usage"] == "LAYER2":
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
row.prop(data=cls.props, property="extrusion_depth", text="Height:")
op = row.operator("bim.change_extrusion_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="length", text="Length:")
op = row.operator("bim.change_layer_length", icon="FILE_REFRESH", text="")
op.length = cls.props.length
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="Angle:")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
row = cls.layout.row()
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__ + "\n⇧ G", ui_context)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(
row,
"Extend",
"S_E",
"Extends/reduces element to 3D cursor\n⇧ E",
ui_context,
)
add_layout_hotkey_operator(
row,
"Butt",
"S_T",
"Intersects two non-parallel elements to a butt corner junction\n⇧ T",
ui_context,
)
add_layout_hotkey_operator(
row,
"Mitre",
"S_Y",
"Intersects two non-parallel elements to a mitred corner junction\n⇧ Y",
ui_context,
)
row.operator("bim.unjoin_walls", text="", icon_value=custom_icon_previews["UNJOIN_WALLS"].icon_id)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__ + "\n⇧ M", ui_context)
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__ + "\n⇧ K", ui_context)
add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__ + "\n⇧ R", ui_context)
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__ + "\n⇧ F", ui_context)
elif AuthoringData.data["active_material_usage"] == "LAYER3":
if len(context.selected_objects) == 1:
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif "LAYER2" in AuthoringData.data["selected_material_usages"]:
add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "", ui_context)
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="Angle")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
elif AuthoringData.data["active_material_usage"] == "PROFILE":
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="cardinal_point", text="Axis")
op = row.operator("bim.change_cardinal_point", icon="FILE_REFRESH", text="")
op.cardinal_point = int(cls.props.cardinal_point)
row = cls.layout.row(align=True)
label = (
"Height" if AuthoringData.data["active_class"] in ("IfcColumn", "IfcColumnStandardCase") else "Length"
)
row.prop(data=cls.props, property="extrusion_depth", text=label)
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(
cls.layout,
"Extend",
"S_E",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
"IfcCableSegment",
"IfcDuctSegment",
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "", ui_context)
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_obstruction")
else:
add_layout_hotkey_operator(
cls.layout,
"Edit Axis",
"A_E",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Butt",
"S_T",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Mitre",
"S_Y",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
add_layout_hotkey_operator(
cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__, ui_context
)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif (
tool.Model.is_parametric_railing_active() and not context.active_object.BIMRailingProperties.is_editing_path
):
# NOTE: should be above "active_representation_type" = "SweptSolid" check
# because it could be a SweptSolid too
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_TAB")
row.operator("bim.enable_editing_railing_path", text="Edit Railing Path")
elif AuthoringData.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
"IfcDoor",
"IfcDoorStandardCase",
):
if AuthoringData.data["active_class"] in ("IfcWindow", "IfcWindowStandardCase"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl2", text="RL")
elif AuthoringData.data["active_class"] in ("IfcDoor", "IfcDoorStandardCase"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL")
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "", ui_context)
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
if not tool.Model.is_parametric_window_active() and not tool.Model.is_parametric_door_active():
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif AuthoringData.data["active_class"] in ("IfcSpace",):
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__, ui_context)
elif tool.Model.is_parametric_roof_active() and not context.active_object.BIMRoofProperties.is_editing_path:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_TAB")
row.operator("bim.enable_editing_roof_path", text="Edit Roof Path")
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
add_layout_hotkey_operator(
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__, ui_context
)
cls.layout.operator("bim.mep_connect_elements")
row = cls.layout.row(align=True)
if len(context.selected_objects) > 1:
row.operator("bim.add_opening", text="", icon_value=custom_icon_previews["VOID"].icon_id)
else:
row.operator("bim.add_potential_opening", text="", icon_value=custom_icon_previews["ADD_VOID"].icon_id)
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
row = cls.layout.row(align=True)
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
if len(context.selected_objects) == 2:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_L")
row.operator("bim.clone_opening", text="Clone Opening")
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Exterior", "S_X", "", ui_context)
add_layout_hotkey_operator(row, "Centerline", "S_C", "", ui_context)
add_layout_hotkey_operator(row, "Interior", "S_V", "", ui_context)
add_layout_hotkey_operator(row, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__, ui_context)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Void", "A_O", "Toggle openings", ui_context)
add_layout_hotkey_operator(row, "Decomposition", "A_D", "Select decomposition", ui_context)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__, ui_context)
add_layout_hotkey_operator(row, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__, ui_context)
row = cls.layout.row()
add_layout_hotkey_operator(
row, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__, ui_context
)
@classmethod
def draw_type_selection_interface(cls):
def draw_container(cls, context):
text = AuthoringData.data["default_container"]
if context.region.type == "UI":
text = f"Container: {text}"
cls.layout.row(align=True).label(text=text, icon="OUTLINER_COLLECTION")
@classmethod
def draw_type_selection_interface(cls, context):
# shared by both sidebar and header
ui_context = str(context.region.type)
row = cls.layout.row(align=True)
row.label(text=f"Container: {AuthoringData.data['default_container']}", icon="OUTLINER_COLLECTION")
if AuthoringData.data["ifc_classes"]:
if ui_context == "UI":
text = "Add"
else:
text = ""
if not AuthoringData.data["ifc_element_type"]:
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "ifc_class", text="")
row.operator("bim.add_constr_type_instance", text=text, icon_value=custom_icon_previews["ADD"].icon_id)
row = cls.layout.row(align=True)
if AuthoringData.data["relating_type_id"]:
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.add_constr_type_instance", text=text, icon_value=custom_icon_previews["ADD"].icon_id)
else:
row.label(text="No Construction Type", icon="FILE_3D")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
@@ -524,7 +824,9 @@ class BimToolUI:
row.prop(cls.props, "type_name")
row = cls.layout.row(align=True)
op = row.operator(
"bim.add_default_type", icon="ADD", text=f"Add {AuthoringData.data['ifc_element_type']}"
"bim.add_default_type",
icon_value=custom_icon_previews["ADD_TYPE"].icon_id,
text=f"Create {AuthoringData.data['ifc_element_type']}",
)
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
@@ -533,9 +835,7 @@ class BimToolUI:
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
cls.draw_type_selection_interface()
def draw_thumbnail(cls):
if AuthoringData.data["ifc_classes"]:
if cls.props.ifc_class:
box = cls.layout.box()
@@ -545,14 +845,6 @@ class BimToolUI:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.hotkey"
+5 -1
View File
@@ -66,4 +66,8 @@ class BIMPatchProperties(PropertyGroup):
ifc_patch_input: StringProperty(default="", name="IFC Patch Input IFC")
ifc_patch_output: StringProperty(default="", name="IFC Patch Output IFC")
ifc_patch_args_attr: CollectionProperty(type=Attribute, name="Arguments")
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
should_load_from_memory: BoolProperty(
default=False,
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
@@ -43,6 +43,7 @@ classes = (
operator.LoadLinkedProject,
operator.LoadProject,
operator.LoadProjectElements,
operator.MeasureTool,
operator.NewProject,
operator.QueryLinkedElement,
operator.RefreshClippingPlanes,
@@ -52,6 +52,7 @@ from bpy.app.handlers import persistent
from ifcopenshell.geom import ShapeElementType
from bonsai.bim.module.project.data import LinksData
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator
from typing import Union
@@ -2286,3 +2287,231 @@ if bpy.app.version >= (4, 1, 0):
@classmethod
def poll_drop(cls, context):
return True
class MeasureTool(bpy.types.Operator):
bl_idname = "bim.measure_tool"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Measure Tool"
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
def __init__(self):
self.mousemove_count = 0
self.action_count = 0
self.visible_objs = []
self.objs_2d_bbox = []
self.number_options = {
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
" ",
".",
"+",
"-",
"*",
"/",
"'",
'"',
"=",
}
self.number_input = []
self.number_output = ""
self.number_is_negative = False
self.is_input_on = False
self.input_options = ["D", "A", "X", "Y", "Z"]
self.input_type = "OFF"
self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
self.snap_angle = None
self.snapping_points = []
def recalculate_inputs(self, context):
if self.number_input:
is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type)
self.input_panel[self.input_type] = self.number_output
if not is_valid:
self.report({"WARNING"}, "The number typed is not valid.")
return is_valid
else:
if self.input_type in {"X", "Y", "Z"}:
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
elif self.input_type in {"D", "A"}:
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
return is_valid
def modal(self, context, event):
if not self.is_input_on:
if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
self.mousemove_count += 1
self.is_input_on = False
self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
else:
self.mousemove_count = 0
if self.mousemove_count == 2:
self.objs_2d_bbox = []
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
if self.mousemove_count > 3:
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
return {"RUNNING_MODAL"}
if event.value == "RELEASE" and event.type == "BACK_SPACE":
tool.Snap.remove_last_polyline_point()
tool.Blender.update_viewport()
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
tool.Snap.insert_polyline_point(self.input_panel)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "C":
tool.Snap.close_polyline()
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if self.is_input_on and event.value == "PRESS" and event.type == "TAB":
self.recalculate_inputs(context)
index = self.input_options.index(self.input_type)
size = len(self.input_options)
self.input_type = self.input_options[((index + 1) % size)]
self.number_input = []
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB":
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "D"
self.number_input = []
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on and event.ascii in self.number_options:
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "D"
self.number_input = []
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type in self.input_options and not event.shift:
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = event.type
self.number_input = []
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if self.input_type in self.input_options:
if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"):
if event.type == "BACK_SPACE":
if len(self.number_input) <= 1:
self.number_input = []
else:
self.number_input = self.number_input[:-1]
self.number_output = "".join(self.number_input)
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
else:
self.number_input.append(event.ascii)
self.number_output = "".join(self.number_input)
if self.number_input:
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
is_valid = self.recalculate_inputs(context)
if is_valid:
tool.Snap.insert_polyline_point(self.input_panel)
self.is_input_on = False
self.input_type = "OFF"
self.number_input = []
self.number_output = ""
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "M":
self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
tool.Blender.update_viewport()
if event.shift and event.value == "PRESS" and event.type == "X":
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("YZ")
if event.shift and event.value == "PRESS" and event.type == "Y":
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("XZ")
if event.shift and event.value == "PRESS" and event.type == "Z":
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("XY")
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"}
if self.is_input_on:
if event.value == "RELEASE" and event.type in {"ESC"}:
self.is_input_on = False
self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
else:
if event.value == "RELEASE" and event.type in {"ESC"}:
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
return {"CANCELLED"}
return {"RUNNING_MODAL"}
def invoke(self, context, event):
if context.space_data.type == "VIEW_3D":
PolylineDecorator.install(context)
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("No Plane")
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
else:
self.report({"WARNING"}, "Active space must be a View3d")
return {"CANCELLED"}
@@ -37,6 +37,7 @@ class ExploreTool(bpy.types.WorkSpaceTool):
("bim.explore_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.explore_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.explore_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
)
def draw_settings(context, layout, ws_tool):
@@ -57,6 +58,9 @@ class ExploreTool(bpy.types.WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Measure Tool", icon="EVENT_M")
class ExploreHotkey(bpy.types.Operator):
@@ -88,3 +92,6 @@ class ExploreHotkey(bpy.types.Operator):
bpy.ops.bim.disable_culling()
else:
bpy.ops.bim.enable_culling("INVOKE_DEFAULT")
def hotkey_S_M(self):
bpy.ops.bim.measure_tool("INVOKE_DEFAULT")
@@ -41,6 +41,7 @@ classes = (
prop.RenameProperties,
prop.AddEditProperties,
prop.DeletePsets,
prop.GlobalPsetProperties,
ui.BIM_PT_object_psets,
ui.BIM_PT_object_qtos,
ui.BIM_PT_material_psets,
@@ -73,6 +74,7 @@ def register():
bpy.types.Scene.RenameProperties = bpy.props.CollectionProperty(type=prop.RenameProperties)
bpy.types.Scene.AddEditProperties = bpy.props.CollectionProperty(type=prop.AddEditProperties)
bpy.types.Scene.DeletePsets = bpy.props.CollectionProperty(type=prop.DeletePsets)
bpy.types.Scene.GlobalPsetProperties = bpy.props.PointerProperty(type=prop.GlobalPsetProperties)
def unregister():
@@ -88,3 +90,4 @@ def unregister():
del bpy.types.Scene.RenameProperties
del bpy.types.Scene.AddEditProperties
del bpy.types.Scene.DeletePsets
del bpy.types.Scene.GlobalPsetProperties
@@ -279,3 +279,8 @@ class AddEditProperties(PropertyGroup):
class DeletePsets(PropertyGroup):
pset_name: StringProperty(name="Pset")
class GlobalPsetProperties(PropertyGroup):
pset_filter: StringProperty(name="Pset Filter", options={"TEXTEDIT_UPDATE"})
qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"})
+80 -9
View File
@@ -100,7 +100,9 @@ def draw_psetqto_ui(
layout: bpy.types.UILayout,
obj_type: tool.Ifc.OBJECT_TYPE,
allow_removing: bool = True,
filter_keyword: str = "",
) -> None:
filter_keyword = filter_keyword.lower()
box = layout.box()
row = box.row(align=True)
if "is_expanded" not in pset:
@@ -158,17 +160,24 @@ def draw_psetqto_ui(
prop["NominalValue"] is None or prop["NominalValue"] == ""
):
continue
nominal_value = str(prop["NominalValue"])
if (
filter_keyword
and filter_keyword not in prop["Name"].lower()
and filter_keyword not in nominal_value.lower()
):
continue
has_props_displayed = True
row = box.row(align=True)
row.scale_y = 0.8
row.label(text=prop["Name"])
op = row.operator("bim.select_similar", text=str(prop["NominalValue"]), icon="NONE", emboss=False)
op.key = pset["Name"]
op = row.operator("bim.select_similar", text=nominal_value, icon="NONE", emboss=False)
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
if not has_props_displayed:
row = box.row()
row.scale_y = 0.8
row.label(text="No Properties Set")
row.label(text="No Properties")
def draw_psetqto_editable_ui(
@@ -185,6 +194,12 @@ class BIM_PT_object_psets(Panel):
bl_region_type = "WINDOW"
bl_context = "object"
bl_parent_id = "BIM_PT_tab_object_metadata"
bl_options = {"HEADER_LAYOUT_EXPAND"}
def draw_header(self, context):
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
row.prop(context.scene.GlobalPsetProperties, "pset_filter", text="", icon="VIEWZOOM")
@classmethod
def poll(cls, context):
@@ -209,7 +224,15 @@ class BIM_PT_object_psets(Panel):
op.obj_type = "Object"
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
draw_psetqto_ui(
context,
0,
{},
props,
self.layout,
"Object",
filter_keyword=context.scene.GlobalPsetProperties.pset_filter,
)
if ObjectPsetsData.data["psets"]:
if ObjectPsetsData.data["is_occurrence"]:
@@ -217,12 +240,29 @@ class BIM_PT_object_psets(Panel):
else:
self.layout.label(text="Type Properties:")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
draw_psetqto_ui(
context,
pset["id"],
pset,
props,
self.layout,
"Object",
filter_keyword=context.scene.GlobalPsetProperties.pset_filter,
)
if ObjectPsetsData.data["inherited_psets"]:
self.layout.label(text="Inherited Type Properties:", icon="CON_CHILDOF")
for pset in ObjectPsetsData.data["inherited_psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
draw_psetqto_ui(
context,
pset["id"],
pset,
props,
self.layout,
"Object",
allow_removing=False,
filter_keyword=context.scene.GlobalPsetProperties.pset_filter,
)
class BIM_PT_object_qtos(Panel):
@@ -232,6 +272,12 @@ class BIM_PT_object_qtos(Panel):
bl_region_type = "WINDOW"
bl_context = "object"
bl_parent_id = "BIM_PT_tab_object_metadata"
bl_options = {"HEADER_LAYOUT_EXPAND"}
def draw_header(self, context):
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
row.prop(context.scene.GlobalPsetProperties, "qto_filter", text="", icon="VIEWZOOM")
@classmethod
def poll(cls, context):
@@ -256,7 +302,15 @@ class BIM_PT_object_qtos(Panel):
op.obj_type = "Object"
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
draw_psetqto_ui(
context,
0,
{},
props,
self.layout,
"Object",
filter_keyword=context.scene.GlobalPsetProperties.qto_filter,
)
if ObjectQtosData.data["qtos"]:
if ObjectQtosData.data["is_occurrence"]:
@@ -264,12 +318,29 @@ class BIM_PT_object_qtos(Panel):
else:
self.layout.label(text="Type Quantities:")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
draw_psetqto_ui(
context,
qto["id"],
qto,
props,
self.layout,
"Object",
filter_keyword=context.scene.GlobalPsetProperties.qto_filter,
)
if ObjectQtosData.data["inherited_qsets"]:
self.layout.label(text="Inherited Type Quantities:", icon="CON_CHILDOF")
for qset in ObjectQtosData.data["inherited_qsets"]:
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
draw_psetqto_ui(
context,
qset["id"],
qset,
props,
self.layout,
"Object",
allow_removing=False,
filter_keyword=context.scene.GlobalPsetProperties.qto_filter,
)
class BIM_PT_material_psets(Panel):
@@ -172,7 +172,13 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMRootProperties
objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects or [context.active_object]
objects: list[bpy.types.Object] = []
if self.obj:
objects = [bpy.data.objects[self.obj]]
elif objects := context.selected_objects:
pass
elif obj := context.active_object:
objects = [obj]
if not objects:
self.report({"INFO"}, "No objects selected.")
@@ -63,6 +63,7 @@ class ColourByPropertyData:
cls.is_loaded = True
cls.data = {}
cls.data["saved_colourschemes"] = cls.saved_colourschemes()
cls.data["colourscheme_key"] = cls.colourscheme_key()
@classmethod
def saved_colourschemes(cls):
@@ -81,6 +82,27 @@ class ColourByPropertyData:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@classmethod
def colourscheme_key(cls):
default = [("QUERY", "Custom Query", "Specify a custom query to colour by"), None]
obj = bpy.context.active_object
if not obj:
return default
element = tool.Ifc.get_entity(obj)
if not element:
return default
keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()]
psets = ifcopenshell.util.element.get_psets(element)
for pset, properties in psets.items():
if pset.endswith("Common"):
keys.extend([f'/.*Common/."{name}"' for name in properties.keys() if name != "id"])
elif pset.endswith("BaseQuantities"):
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
else:
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
results = [(k, k, "") for k in keys]
return default + results
class SelectSimilarData:
data = {}
+109 -31
View File
@@ -18,15 +18,16 @@
import bpy
import json
import bisect
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.selector
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
import bonsai.core.search as core
from itertools import cycle
from bonsai.bim.ifc import IfcStore
from natsort import natsorted
from bpy.types import PropertyGroup, Operator
from bpy.props import (
PointerProperty,
@@ -40,22 +41,6 @@ from bpy.props import (
)
colour_list = [
(0.651, 0.81, 0.892, 1),
(0.121, 0.471, 0.706, 1),
(0.699, 0.876, 0.54, 1),
(0.199, 0.629, 0.174, 1),
(0.983, 0.605, 0.602, 1),
(0.89, 0.101, 0.112, 1),
(0.989, 0.751, 0.427, 1),
(0.986, 0.497, 0.1, 1),
(0.792, 0.699, 0.839, 1),
(0.414, 0.239, 0.603, 1),
(0.993, 0.999, 0.6, 1),
(0.693, 0.349, 0.157, 1),
]
class AddFilterGroup(Operator):
bl_idname = "bim.add_filter_group"
bl_label = "Add Filter Group"
@@ -139,6 +124,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
bl_description = "Edit the underlying filter query for advanced users"
bl_options = {"REGISTER", "UNDO"}
query: StringProperty(name="Query")
old_query: StringProperty(name="Old Query")
module: StringProperty()
def _execute(self, context):
@@ -264,33 +250,91 @@ class ColourByProperty(Operator):
def _execute(self, context):
props = context.scene.BIMSearchProperties
query = props.colourscheme_query
query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key
if not query:
self.report({"ERROR"}, "No Query Provided")
return {"CANCELLED"}
colours = cycle(colour_list)
palette = props.palette
is_qualitative = palette in ("tab10", "paired")
if is_qualitative:
colours = tool.Search.get_qualitative_palette(palette)
colourscheme = {}
if len(props.colourscheme):
obj_values = {}
min_mode = props.min_mode
max_mode = props.max_mode
min_value = props.min_value if min_mode == "MANUAL" else None
max_value = props.max_value if max_mode == "MANUAL" else None
if is_qualitative and len(props.colourscheme):
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
value = str(ifcopenshell.util.selector.get_element_value(element, query))
if value in colourscheme:
colourscheme[value]["total"] += 1
value = ifcopenshell.util.selector.get_element_value(element, query)
if is_qualitative:
value = str(value)
if value in colourscheme:
colourscheme[value]["total"] += 1
else:
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
obj.color = (*colourscheme[value]["colour"], 1)
else:
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
obj.color = (*colourscheme[value]["colour"], 1)
if value is None:
obj.color = (0, 0, 0, 1)
if "None" in colourscheme:
colourscheme["None"]["total"] += 1
else:
colourscheme["None"] = {"colour": (0, 0, 0), "total": 1}
else:
try:
value = float(value)
if min_mode == "AUTO":
if min_value is None or value < min_value:
min_value = value
if max_mode == "AUTO":
if max_value is None or value > max_value:
max_value = value
obj_values[obj] = value
except:
obj.color = (0, 0, 0, 1)
if not is_qualitative:
steps = 10 if max_value is not None and min_value is not None else 0
step_size = (max_value - min_value) / (steps - 1)
values = []
for i in range(steps):
step_value = min_value + i * step_size
values.append(step_value)
colourscheme[str(step_value)] = {
"colour": tool.Search.get_quantitative_palette(palette, step_value, min_value, max_value),
"total": 0,
}
for obj, value in obj_values.items():
index = bisect.bisect_right(values, value)
if index >= len(values):
index = -1
colourscheme[str(values[index])]["total"] += 1
obj.color = (*tool.Search.get_quantitative_palette(palette, value, min_value, max_value), 1)
if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]:
areas[0].spaces[0].shading.color_type = "OBJECT"
props.colourscheme.clear()
for value in sorted(colourscheme.keys()):
if is_qualitative:
keys = natsorted(colourscheme.keys())
else:
keys = sorted(colourscheme.keys(), key=self.sort_quantitative_key)
for value in keys:
data = colourscheme[value]
new = props.colourscheme.add()
new.name = str(value)
@@ -298,6 +342,12 @@ class ColourByProperty(Operator):
new.colour = data["colour"][0:3]
return {"FINISHED"}
def sort_quantitative_key(self, value):
try:
return (0, float(value))
except ValueError:
return (1, value)
def store_state(self, context):
if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]:
self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type}
@@ -323,21 +373,49 @@ class SelectByProperty(Operator):
def execute(self, context):
props = context.scene.BIMSearchProperties
query = props.colourscheme_query
query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key
if not query:
self.report({"ERROR"}, "No Query Provided")
return {"CANCELLED"}
active_value = props.colourscheme[props.active_colourscheme_index].name
palette = props.palette
is_qualitative = palette in ("tab10", "paired")
if not is_qualitative:
values = []
for colour in props.colourscheme:
try:
values.append(float(colour.name))
except:
pass
values = sorted(values)
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
value = str(ifcopenshell.util.selector.get_element_value(element, query))
if value == active_value:
obj.select_set(True)
value = ifcopenshell.util.selector.get_element_value(element, query)
if is_qualitative:
if str(value) == active_value:
obj.select_set(True)
else:
if active_value == "None":
if value is None:
obj.select_set(True)
else:
try:
value = float(value)
index = bisect.bisect_right(values, value)
if index >= len(values):
index = -1
if values[index] == float(active_value):
obj.select_set(True)
except:
pass
return {"FINISHED"}
@@ -41,6 +41,12 @@ def get_element_key(self, context):
return SelectSimilarData.data["element_key"]
def get_colourscheme_key(self, context):
if not ColourByPropertyData.is_loaded:
ColourByPropertyData.load()
return ColourByPropertyData.data["colourscheme_key"]
def get_saved_searches(self, context):
if not SearchData.is_loaded:
SearchData.load()
@@ -81,6 +87,21 @@ def update_is_container_selected(self, context):
new.obj = obj
def update_show_flat_colours(self, context):
if self.show_flat_colours:
space = tool.Blender.get_view3d_space()
space.shading.light = "FLAT"
space.shading.color_type = "OBJECT"
space.shading.show_object_outline = True
space.shading.show_cavity = True
else:
space = tool.Blender.get_view3d_space()
space.shading.type = "SOLID"
space.shading.light = "STUDIO"
space.shading.show_object_outline = True
space.shading.show_cavity = False
class BIMFilterClasses(PropertyGroup):
name: StringProperty(name="Name")
is_selected: BoolProperty(name="Is Selected", default=True, update=update_is_class_selected)
@@ -122,7 +143,43 @@ class BIMSearchProperties(PropertyGroup):
)
saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches")
saved_colourschemes: EnumProperty(items=get_saved_colourschemes, name="Saved Colourschemes")
colourscheme_key: EnumProperty(items=get_colourscheme_key, name="Colourscheme Key")
colourscheme_query: StringProperty(name="Colourscheme Query", default="class")
palette: EnumProperty(
items=[
("tab10", "Default (Qualitative)", "10 Contrasting colours to distinguish categories"),
("paired", "Paired (Qualitative)", "12 Contrasting colour pairs to distinguish categories"),
("rocket", "Rocket (Quantitative - Sequential)", "A sequential range from black to red to white"),
("mako", "Mako (Quantitative - Sequential)", "A sequential range from black to blue to white"),
(
"coolwarm",
"CoolWarm (Quantitative - Diverging)",
"A diverging linear range from blue to red with white in the middle",
),
(
"spectral",
"Spectral (Quantitative - Diverging)",
"A diverging spectral range from red to blue with white in the middle",
),
],
name="Palette",
)
min_mode: EnumProperty(
items=[
("AUTO", "Automatic", "Automatically determine the minimum value"),
("MANUAL", "Manual", "Manually specify the minimum value"),
],
name="Min Mode",
)
max_mode: EnumProperty(
items=[
("AUTO", "Automatic", "Automatically determine the maximum value"),
("MANUAL", "Manual", "Manually specify the maximum value"),
],
name="Max Mode",
)
min_value: FloatProperty(name="Min Value", default=0)
max_value: FloatProperty(name="Max Value", default=100)
colourscheme: CollectionProperty(type=BIMColour)
active_colourscheme_index: IntProperty(name="Active Colourscheme Index")
filter_type: StringProperty(name="Filter Type")
@@ -130,6 +187,7 @@ class BIMSearchProperties(PropertyGroup):
filter_classes_index: IntProperty(name="Filter Classes Index")
filter_container: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
filter_container_index: IntProperty(name="Filter Level Index")
show_flat_colours: BoolProperty(name="Flat Colours", default=False, update=update_show_flat_colours)
def get_classes(self, ifc_product):
+20 -1
View File
@@ -83,11 +83,30 @@ class BIM_PT_colour_by_property(Panel):
row.operator("bim.save_colourscheme", text="", icon="EXPORT")
row = self.layout.row()
row.prop(props, "colourscheme_query", text="Query")
bonsai.bim.helper.prop_with_search(self.layout, props, "colourscheme_key", text="Colour By")
if props.colourscheme_key == "QUERY":
row = self.layout.row()
row.prop(props, "colourscheme_query", text="Query")
row = self.layout.row()
row.prop(props, "palette")
if props.palette not in ("tab10", "paired"):
row = self.layout.row(align=True)
row.prop(props, "min_mode", text="Min Value")
if props.min_mode == "MANUAL":
row.prop(props, "min_value", text="")
row = self.layout.row(align=True)
row.prop(props, "max_mode", text="Max Value")
if props.max_mode == "MANUAL":
row.prop(props, "max_value", text="")
row = self.layout.row(align=True)
row.operator("bim.colour_by_property", icon="BRUSH_DATA")
row.operator("bim.reset_object_colours")
row.prop(props, "show_flat_colours", icon="SHADING_RENDERED", text="")
row.operator("bim.select_by_property", icon="RESTRICT_SELECT_OFF", text="")
if len(props.colourscheme):
@@ -249,8 +249,6 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
container: bpy.props.IntProperty()
ifc_class: bpy.props.StringProperty()
relating_type: bpy.props.IntProperty()
@classmethod
def description(cls, context, operator):
@@ -262,20 +260,25 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context):
ifc_class = relating_type = is_untyped = None
if self.should_filter:
ifc_class = self.ifc_class
relating_type = tool.Ifc.get().by_id(self.relating_type) if self.relating_type else None
is_untyped = self.relating_type == 0
else:
ifc_class = ""
relating_type = None
is_untyped = False
active_element = context.scene.BIMSpatialDecompositionProperties.active_element
if active_element.is_class:
ifc_class = active_element.name
elif relating_type := active_element.ifc_definition_id:
ifc_class = active_element.ifc_class
relating_type = tool.Ifc.get().by_id(relating_type)
else:
ifc_class = active_element.ifc_class
is_untyped = True
element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter
core.select_decomposed_elements(
tool.Spatial,
container=tool.Ifc.get().by_id(self.container),
ifc_class=ifc_class,
relating_type=relating_type,
is_untyped=is_untyped,
element_filter = element_filter
)
@@ -107,6 +107,7 @@ class BIMContainer(PropertyGroup):
class Element(PropertyGroup):
name: StringProperty(name="Name")
ifc_class: StringProperty(name="Name")
is_class: BoolProperty(name="Is Class", default=False)
is_type: BoolProperty(name="Is Type", default=False)
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -114,10 +115,12 @@ class Element(PropertyGroup):
class BIMSpatialDecompositionProperties(PropertyGroup):
container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
containers: CollectionProperty(name="Containers", type=BIMContainer)
contracted_containers: StringProperty(name="Contracted containers", default="[]")
expanded_containers: StringProperty(name="Expanded containers", default="[]")
active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index)
element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"})
elements: CollectionProperty(name="Elements", type=Element)
active_element_index: IntProperty(name="Active Element Index")
total_elements: IntProperty(name="Total Elements")
+60 -18
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.module.spatial.data import SpatialData, SpatialDecompositionData
import bonsai.tool as tool
@@ -169,12 +170,6 @@ class BIM_PT_spatial_decomposition(Panel):
)
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
if self.props.active_element.is_class:
op.ifc_class = self.props.active_element.name
op.relating_type = 0
elif self.props.active_element.is_type:
op.ifc_class = ""
op.relating_type = self.props.active_element.ifc_definition_id
op.container = ifc_definition_id
self.layout.template_list(
@@ -184,10 +179,14 @@ class BIM_PT_spatial_decomposition(Panel):
"elements",
self.props,
"active_element_index",
sort_lock=True,
)
class BIM_UL_containers_manager(UIList):
def __init__(self):
self.use_filter_show = True
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
@@ -213,22 +212,52 @@ class BIM_UL_containers_manager(UIList):
col.prop(item, "elevation", emboss=False, text="")
def draw_hierarchy(self, row, item):
for i in range(0, item.level_index):
row.label(text="", icon="BLANK1")
if item.has_children:
if item.is_expanded:
row.operator("bim.contract_container", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").container = (
item.ifc_definition_id
)
if item.level_index:
for i in range(0, item.level_index - 1):
row.label(text="", icon="BLANK1")
if item.has_children:
if item.is_expanded:
row.operator(
"bim.contract_container", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).container = item.ifc_definition_id
else:
row.operator(
"bim.expand_container", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).container = item.ifc_definition_id
else:
row.operator("bim.expand_container", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").container = (
item.ifc_definition_id
)
else:
row.label(text="", icon="BLANK1")
row.label(text="", icon="BLANK1")
def draw_filter(self, context, layout):
row = layout.row()
row.prop(context.scene.BIMSpatialDecompositionProperties, "container_filter", text="", icon="VIEWZOOM")
def filter_items(self, context, data, propname):
items = getattr(data, propname)
filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter.lower()
filter_flags = [self.bitflag_filter_item] * len(items)
for idx, item in enumerate(items):
if (
filter_name in item.name.lower()
or filter_name in item.long_name.lower()
or filter_name in item.ifc_class.lower()
):
filter_flags[idx] |= self.bitflag_filter_item
else:
filter_flags[idx] &= ~self.bitflag_filter_item
return filter_flags, []
items = getattr(data, propname)
filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter
filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name")
return filtered, []
class BIM_UL_elements(UIList):
def __init__(self):
self.use_filter_show = True
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
@@ -244,3 +273,16 @@ class BIM_UL_elements(UIList):
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
def draw_filter(self, context, layout):
row = layout.row()
row.prop(context.scene.BIMSpatialDecompositionProperties, "element_filter", text="", icon="VIEWZOOM")
def filter_items(self, context, data, propname):
items = getattr(data, propname)
filter_name = context.scene.BIMSpatialDecompositionProperties.element_filter
filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name")
return filtered, []
def get_filter_name(self):
return self.filter_name

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