Compare commits
229 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b8af680bc | |||
| 8a57a52949 | |||
| 720eda51c3 | |||
| 01a701007e | |||
| ec5368224f | |||
| 56e9488d3e | |||
| 23ec2fc48e | |||
| fbcb19fdbb | |||
| d9f3c9d012 | |||
| 91e236c42c | |||
| 03ab88b5bb | |||
| 42ef06aae1 | |||
| 691dfb45f0 | |||
| 760816b7f0 | |||
| fc04c6e41e | |||
| 99e67d528f | |||
| c113061742 | |||
| 174ff00a6d | |||
| 84a2d4266d | |||
| 64cba98c3d | |||
| 1aed2a6b3c | |||
| 4e1a956830 | |||
| 747a39b440 | |||
| 1b49515d9f | |||
| dabb85c466 | |||
| b74811b3f4 | |||
| 79eaf895f7 | |||
| 994c85a05a | |||
| 0faacccf6d | |||
| 89d27bdf5f | |||
| 76853db32c | |||
| 002c50e29d | |||
| d2efb2ab78 | |||
| 8bb78959ef | |||
| 48f0dc6a00 | |||
| 27aee31adf | |||
| 24aaefb927 | |||
| 7daec25454 | |||
| 8309b13db0 | |||
| 5e95985d19 | |||
| fefe325ece | |||
| b717abf639 | |||
| 32ae12b45a | |||
| 48b74ebb76 | |||
| 197d0b3258 | |||
| 2a1bc30f19 | |||
| 6dcbd98769 | |||
| 1be025ed86 | |||
| 6fa87a16ab | |||
| 8c19b695ea | |||
| 5831c56380 | |||
| 3dde1b80b9 | |||
| 08dd8f87ff | |||
| 1c3cf44122 | |||
| 868454c011 | |||
| edcfcd8850 | |||
| 33bbf9fc53 | |||
| 286e90e299 | |||
| 5aa7758795 | |||
| 6c47baf0de | |||
| 51018177cd | |||
| be9719f5ca | |||
| 9193ff7252 | |||
| 53c3019b73 | |||
| 6bbf3d3917 | |||
| 47a15011f6 | |||
| f26f6dfe5e | |||
| 1745a4fd39 | |||
| fcaac3787b | |||
| 2d3adce59e | |||
| 7e6607adcf | |||
| 46c62d7cdb | |||
| c521892ca2 | |||
| d4a0dd1527 | |||
| 527cdd3304 | |||
| 31e836cfb4 | |||
| 86f123b6b0 | |||
| fea8e3a5c9 | |||
| 3137904055 | |||
| f89dee7668 | |||
| c042d1afe5 | |||
| 42fc72bd4f | |||
| f72c128ba6 | |||
| 3ceeaa739f | |||
| 7a2e58944e | |||
| b9855b289f | |||
| c7a8fd9309 | |||
| daa6b31c5d | |||
| cd411e1f14 | |||
| 7a168069f7 | |||
| 97d2e08286 | |||
| f440a3ca17 | |||
| 81bdb551ab | |||
| 5fdad744d3 | |||
| ab6f408db4 | |||
| cdf8b9c038 | |||
| 918fb41a7e | |||
| 1d8e39e55f | |||
| b0b65e550c | |||
| a1527a7e31 | |||
| 2ca3f86fc9 | |||
| d80bcd1a94 | |||
| 60a06138d2 | |||
| e2001e82dd | |||
| 92f4cb0ffa | |||
| 9f00e8aef0 | |||
| 638d200a27 | |||
| 59717d6e92 | |||
| 009795f7dc | |||
| a0792a4b93 | |||
| 8534b6e154 | |||
| 7910a68028 | |||
| d44ee9874b | |||
| 8cd86581ff | |||
| 0a5b6a41b0 | |||
| 7bac468151 | |||
| b375579dcd | |||
| d3d1ca0169 | |||
| 2b0572af10 | |||
| ed6365b292 | |||
| e3b1e0773f | |||
| 3d38068b90 | |||
| a4a2497f33 | |||
| 1ef2fb89af | |||
| 902d777574 | |||
| 3bd0caade1 | |||
| 301cb4bbdc | |||
| f0338ffdda | |||
| 2100528cbe | |||
| ae4f681f73 | |||
| 53d7624fc4 | |||
| 951b2b76a6 | |||
| e9b4b6560e | |||
| cabffdc549 | |||
| 7ea60742d6 | |||
| 358c834427 | |||
| 5d6655cbc7 | |||
| 4d122d77dc | |||
| d4e863ed3b | |||
| b6ccf505e7 | |||
| ce9842983b | |||
| eb19b26a6e | |||
| 06602d0e39 | |||
| 00279b63de | |||
| 6418353119 | |||
| 3afbbe51d2 | |||
| ddb767674b | |||
| 5575ea8296 | |||
| e8a7a032aa | |||
| f23e419685 | |||
| b87e8136af | |||
| 473d0ae888 | |||
| 94f5137684 | |||
| 4e75945435 | |||
| e31019eb67 | |||
| c505630a20 | |||
| 860fa81f73 | |||
| 847b5d226e | |||
| ad77d09916 | |||
| 9b12cae8b4 | |||
| 84a8c5dfeb | |||
| 12a7b8a79e | |||
| ef04402ed8 | |||
| 45fed747c2 | |||
| 85229fc865 | |||
| 916f854905 | |||
| 1915541d20 | |||
| e37b84a95a | |||
| bcb2a37ce0 | |||
| 018d6847a0 | |||
| 35918265d0 | |||
| 4e48c35425 | |||
| d985fe9240 | |||
| 7c00021aaa | |||
| d2dc8b519f | |||
| 25fe025354 | |||
| 1aae710286 | |||
| 38e8130793 | |||
| 7e94388074 | |||
| ef835e43b8 | |||
| e5d0eb13bf | |||
| 4fcf23319f | |||
| 99154afdb1 | |||
| e83eb68fe1 | |||
| e4a4a33920 | |||
| e8848c9701 | |||
| cfd8044cb3 | |||
| 2d7866570e | |||
| eb0ed7fe6b | |||
| 67733c4449 | |||
| b3940cdc45 | |||
| 94e75aebb0 | |||
| ff203b2a13 | |||
| 10616df494 | |||
| 1dc6d4935e | |||
| 04088af2b6 | |||
| c477173096 | |||
| 2f4093f958 | |||
| 95fc51da76 | |||
| 50e629c02c | |||
| 4a178fca58 | |||
| aff5b238d9 | |||
| 315b6060b0 | |||
| b137197586 | |||
| d1d9db3ed8 | |||
| 736b50c6fa | |||
| 64b96633c2 | |||
| 8eae997053 | |||
| fee56fc120 | |||
| d1f87d5467 | |||
| 7539404c27 | |||
| 71ddc6384a | |||
| 4b402f8ea5 | |||
| c39fa80e09 | |||
| c89ad8c7da | |||
| 6dfe46d9bd | |||
| 1de51ecf77 | |||
| 8a55f18c95 | |||
| 7a1976cfc1 | |||
| 78c6401453 | |||
| e12838d93e | |||
| 7aa5b71a6a | |||
| 2ef3af5e1b | |||
| d399f7c6b7 | |||
| beb7f75368 | |||
| dc6fdc2a98 | |||
| 45be17c25a | |||
| 4f11b19b3d | |||
| f646cf4372 |
@@ -142,6 +142,12 @@ jobs:
|
||||
sverchok_zip="$(pwd)/dist/$(ls dist)"
|
||||
blender --command extension install-file -r user_default $sverchok_zip
|
||||
|
||||
# Install Sun Position extension.
|
||||
blender --online-mode --command extension sync
|
||||
blender --online-mode --background --python-expr "import bpy; \
|
||||
bpy.ops.extensions.package_install(repo_index=0, pkg_id='sun_position'); \
|
||||
bpy.ops.preferences.addon_enable(module='bl_ext.blender_org.sun_position'); bpy.ops.wm.save_userpref()"
|
||||
|
||||
cd ../bonsai
|
||||
pip install pytest-blender
|
||||
blender --background --python scripts/setup_pytest.py
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending
|
||||
is possible at compile-time when using C++ and at run-time when using Python.
|
||||
|
||||
In addition to a C++ and Python API, IfcOpenShell comes with an ecosystem of tools, notably including IfcConvert (an application
|
||||
to convert IFC models to other formats), the BlenderBIM Add-on (an add-on to Blender providing a graphical IFC authoring platform),
|
||||
to convert IFC models to other formats), the ~~BlenderBIM~~ Bonsai Add-on (an add-on to Blender providing a graphical IFC authoring platform),
|
||||
and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF and IDS.
|
||||
|
||||
For more information, see:
|
||||
@@ -37,7 +37,7 @@ Contents
|
||||
| Name | Description | License | Service |
|
||||
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
|
||||
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) |
|
||||
| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| ~~blenderbim~~ Bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
|
||||
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
||||
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
|
||||
endif
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=d51fa2c
|
||||
OLD:=7e6607a
|
||||
.PHONY: bump
|
||||
bump:
|
||||
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 767 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 689 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 971 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 863 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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):
|
||||
@@ -44,9 +38,9 @@ class WebNamespace(socketio.AsyncNamespace):
|
||||
|
||||
async def on_connect(self, sid, environ):
|
||||
print(f"Web client connected: {sid}")
|
||||
await sio.emit("theme_data", blender_theme, namespace="/web", room=sid)
|
||||
if blender_messages:
|
||||
await sio.emit("connected_clients", list(blender_messages.keys()), namespace="/web", room=sid)
|
||||
await sio.emit("theme_data", blender_theme, namespace="/web", room=sid)
|
||||
await self.send_cached_messages(sid)
|
||||
|
||||
async def on_disconnect(self, sid):
|
||||
@@ -61,7 +55,6 @@ class WebNamespace(socketio.AsyncNamespace):
|
||||
)
|
||||
|
||||
async def on_get_svg(self, sid, data):
|
||||
print("hello world!")
|
||||
file_path = data["path"]
|
||||
with open(file_path, "r") as file:
|
||||
svg_data = file.read()
|
||||
@@ -74,6 +67,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
|
||||
@@ -117,10 +112,29 @@ 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")
|
||||
|
||||
# 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")
|
||||
|
||||
async def on_work_schedule_info(self, sid, data):
|
||||
print(f"Work schedule info from Blender client {sid}")
|
||||
blender_messages[sid]["work_schedule_info"] = data
|
||||
await sio.emit("work_schedule_info", {"blenderId": sid, "data": data}, namespace="/web")
|
||||
|
||||
async def on_cost_items(self, sid, data):
|
||||
print(f"Cost items data from Blender client {sid}")
|
||||
blender_messages[sid]["cost_items"] = data
|
||||
await sio.emit("cost_items", {"blenderId": sid, "data": data}, namespace="/web")
|
||||
|
||||
async def on_cost_schedules(self, sid, data):
|
||||
print(f"Cost schedule info from Blender client {sid}")
|
||||
blender_messages[sid]["cost_schedules"] = data
|
||||
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
|
||||
|
||||
async def schedules(request):
|
||||
with open("templates/index.html", "r") as f:
|
||||
@@ -128,6 +142,12 @@ async def schedules(request):
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
async def costing(request):
|
||||
with open("templates/costing.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 sequencing(request):
|
||||
with open("templates/gantt.html", "r") as f:
|
||||
@@ -143,6 +163,15 @@ async def documentation(request):
|
||||
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"
|
||||
|
||||
@@ -169,6 +198,8 @@ sio.register_namespace(BlenderNamespace("/blender"))
|
||||
app.router.add_get("/", schedules)
|
||||
app.router.add_get("/documentation", documentation)
|
||||
app.router.add_get("/sequencing", sequencing)
|
||||
app.router.add_get("/costing", costing)
|
||||
app.router.add_get("/demo", demo)
|
||||
|
||||
# Add static files
|
||||
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
:root.blender .flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* CSS for the work schedule cards */
|
||||
:root.blender #work_schedules {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:root.blender .card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 300px;
|
||||
margin: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
:root.blender .card:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
:root.blender .card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:root.blender .card-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
:root.blender .card-text {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:root.blender .btn-primary {
|
||||
background-color: #007bff;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
:root.blender .btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
: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;
|
||||
padding: var(--margin-small);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -55,10 +55,7 @@ body {
|
||||
}
|
||||
|
||||
#container {
|
||||
margin-top: var(--margin-medium);
|
||||
margin-left: var(--margin-tiny);
|
||||
margin-right: var(--margin-tiny);
|
||||
margin-bottom: var(--margin-medium);
|
||||
padding: var(--margin-small);
|
||||
height: calc(100vh - var(--nav-height));
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import url("./components/card.css");
|
||||
|
||||
:root {
|
||||
--font-family: Arial, sans-serif;
|
||||
--base-font-size: 16px;
|
||||
@@ -72,10 +74,7 @@ body {
|
||||
|
||||
#container {
|
||||
flex: 1;
|
||||
margin-top: var(--margin-medium);
|
||||
margin-left: var(--margin-small);
|
||||
margin-right: var(--margin-small);
|
||||
margin-bottom: var(--margin-medium);
|
||||
padding: var(--margin-small);
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
@@ -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 {
|
||||
@@ -204,7 +202,10 @@ footer p {
|
||||
}
|
||||
|
||||
.table-container {
|
||||
margin-bottom: var(--margin-large);
|
||||
display: block;
|
||||
height: calc(100vh - var(--margin-small));
|
||||
width: 100%;
|
||||
margin-bottom: var(--margin-medium);
|
||||
}
|
||||
|
||||
.csv-table {
|
||||
@@ -279,22 +280,29 @@ button:hover {
|
||||
|
||||
|
||||
|
||||
:root.blender .tabulator-selected .tabulator-cell {
|
||||
:root.blender .tabulator-selected .tabulator-cell,
|
||||
:root.blender .tabulator-row.tabulator-selected {
|
||||
background-color: var(--blender-active-highlight) !important;
|
||||
color: var(--blender-active-text-highlight) !important;
|
||||
}
|
||||
|
||||
|
||||
:root.blender .tabulator-selected:hover .tabulator-cell {
|
||||
:root.blender .tabulator-selected:hover .tabulator-cell,
|
||||
:root.blender .tabulator-row.tabulator-selected:hover {
|
||||
filter: brightness(1.2)
|
||||
}
|
||||
|
||||
.tabulator-menu-item {
|
||||
:root.blender .tabulator-popup-container {
|
||||
background: var(--blender-panel-background);
|
||||
border: var(--blender-tab-outline);
|
||||
}
|
||||
|
||||
:root.blender .tabulator-menu-item {
|
||||
color: var(--blender-text) !important;
|
||||
background-color: var(--blender-panel-background) !important;
|
||||
transition: filter 0.1s ease;
|
||||
}
|
||||
|
||||
.tabulator-menu-item:hover {
|
||||
:root.blender .tabulator-menu-item:hover {
|
||||
filter: brightness(1.4);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { CostUI } from './utilities/costui.js';
|
||||
|
||||
const connectedClients = {};
|
||||
let socket;
|
||||
|
||||
$(document).ready(function () {
|
||||
var defaultTheme = "blender";
|
||||
var theme = localStorage.getItem("theme") || defaultTheme;
|
||||
setTheme(theme);
|
||||
|
||||
connectSocket();
|
||||
CostUI.createColorPicker();
|
||||
});
|
||||
|
||||
function connectSocket() {
|
||||
const url = "ws://localhost:" + SOCKET_PORT + "/web";
|
||||
socket = io(url);
|
||||
|
||||
socket.on("blender_connect", handleBlenderConnect);
|
||||
socket.on("blender_disconnect", handleBlenderDisconnect);
|
||||
socket.on("connected_clients", handleConnectedClients);
|
||||
socket.on("theme_data", handleThemeData);
|
||||
socket.on("connect", handleWebConnect);
|
||||
socket.on("cost_schedules", handleCostSchedulesData);
|
||||
socket.on("cost_items", handleCostItemsData);
|
||||
}
|
||||
|
||||
function handleBlenderConnect(blenderId) {
|
||||
if (!connectedClients.hasOwnProperty(blenderId)) {
|
||||
connectedClients[blenderId] = { shown: false, ifc_file: "" };
|
||||
}
|
||||
|
||||
$("#blender-count").text(function (i, text) {
|
||||
return parseInt(text, 10) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function handleBlenderDisconnect(blenderId) {
|
||||
if (connectedClients.hasOwnProperty(blenderId)) {
|
||||
delete connectedClients[blenderId];
|
||||
removeTableElement(blenderId);
|
||||
}
|
||||
|
||||
$("#blender-count").text(function (i, text) {
|
||||
return parseInt(text, 10) - 1;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleConnectedClients(data) {
|
||||
$("#blender-count").text(data.length);
|
||||
|
||||
data.forEach(function (id) {
|
||||
connectedClients[id] = { shown: false, ifc_file: "" };
|
||||
});
|
||||
}
|
||||
|
||||
function handleThemeData(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);
|
||||
|
||||
var styleElement = $("#index-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 addCostItem(costItemId) {
|
||||
console.log("addCostItem", costItemId);
|
||||
executeOperator({ type: "addCostItem", costItemId: costItemId });
|
||||
}
|
||||
|
||||
function editCostItemName(costItemId, name) {
|
||||
executeOperator({ type: "editCostItemName", costItemId: costItemId, name: name });
|
||||
}
|
||||
|
||||
function selectAssignedElements(costItemId) {
|
||||
executeOperator({ type: "selectAssignedElements", costItemId: costItemId });
|
||||
}
|
||||
|
||||
function handleWebConnect() {
|
||||
getCostSchedules();
|
||||
}
|
||||
|
||||
function handleCostSchedulesData(data) {
|
||||
const blenderId = data.blenderId;
|
||||
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
|
||||
const currency = data.data["cost_schedules"]["currency"]["name"];
|
||||
|
||||
console.log(data.data["cost_schedules"]);
|
||||
|
||||
const costScheduleDiv = $("#cost-schedules");
|
||||
|
||||
costSchedules.forEach((costSchedule) => {
|
||||
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
|
||||
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
|
||||
const callback = () => loadCostSchedule(costSchedule.id, blenderId);
|
||||
|
||||
|
||||
const card = CostUI.createCard(costSchedule.Name, mainContainer, callback);
|
||||
costScheduleDiv.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
function handleCostItemsData(data) {
|
||||
console.log(data);
|
||||
CostUI.createCostSchedule({
|
||||
data: data.data["cost_items"],
|
||||
blenderID: data.blenderId,
|
||||
callbacks: {
|
||||
"addCostItem": addCostItem,
|
||||
"selectAssignedElements": selectAssignedElements,
|
||||
'editCostItemName': editCostItemName,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeOperator(operator, blenderId) {
|
||||
const msg = {
|
||||
sourcePage: "cost",
|
||||
operator: operator,
|
||||
};
|
||||
if (blenderId !== undefined) {
|
||||
msg.BlenderId = blenderId;
|
||||
}
|
||||
socket.emit("web_operator", msg);
|
||||
}
|
||||
|
||||
function loadCostSchedule(costScheduleId, blenderId) {
|
||||
executeOperator({ type: "loadCostSchedule", costScheduleId: costScheduleId }, blenderId);
|
||||
}
|
||||
|
||||
function getCostSchedules(blenderId) {
|
||||
executeOperator({ type: "getCostSchedules" }, blenderId);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CostUI } from './utilities/costui.js';
|
||||
|
||||
// keeps track of blenders connected in form of
|
||||
// shown:bool, workSchedule: {}, ganttTasks: {}, warning: bool
|
||||
const connectedClients = {};
|
||||
@@ -36,9 +38,16 @@ function connectSocket() {
|
||||
socket.on("blender_connect", handleBlenderConnect);
|
||||
socket.on("blender_disconnect", handleBlenderDisconnect);
|
||||
socket.on("connected_clients", handleConnectedClients);
|
||||
socket.on("connect", handleWebConnect);
|
||||
socket.on("theme_data", handleThemeData);
|
||||
socket.on("gantt_data", handleGanttData);
|
||||
socket.on("default_data", handleDefaultData);
|
||||
socket.on("work_schedule_info", handleWorkScheduleData);
|
||||
}
|
||||
|
||||
// function used to get drawings data from Bonsai
|
||||
function handleWebConnect() {
|
||||
getWorkScheduleData();
|
||||
}
|
||||
|
||||
// Function to handle 'blender_connect' event
|
||||
@@ -62,6 +71,7 @@ function handleBlenderDisconnect(blenderId) {
|
||||
console.log("blender disconnected: ", blenderId);
|
||||
if (connectedClients.hasOwnProperty(blenderId)) {
|
||||
delete connectedClients[blenderId];
|
||||
removeWorkScheduleInfo(blenderId);
|
||||
removeGanttElement(blenderId);
|
||||
}
|
||||
|
||||
@@ -114,8 +124,25 @@ function handleThemeData(themeData) {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle 'gantt_data' event
|
||||
function handleWorkScheduleData(data) {
|
||||
const blenderId = data["blenderId"];
|
||||
const workSchedulesMainDiv = $("#work_schedules");
|
||||
const workSchedulesDiv = $("<div>").attr("id", "work_schedules-" + blenderId);
|
||||
workSchedulesMainDiv.append(workSchedulesDiv);
|
||||
|
||||
const workSchedules = data["data"]["work_schedule_info"];
|
||||
|
||||
workSchedules.forEach((workSchedule) => {
|
||||
console.log(workSchedule);
|
||||
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
|
||||
const callback = () => loadWorkSchedule(workSchedule.id);
|
||||
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
|
||||
workSchedulesDiv.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
function handleGanttData(data) {
|
||||
console.log("running handleGanttData");
|
||||
const blenderId = data["blenderId"];
|
||||
|
||||
console.log(data);
|
||||
@@ -156,7 +183,7 @@ function handleDefaultData(data) {
|
||||
const blenderId = data["blenderId"];
|
||||
const isDirty = data["data"]["is_dirty"];
|
||||
showWarning(blenderId, isDirty);
|
||||
console.log(data);
|
||||
console.log('default data',data);
|
||||
}
|
||||
|
||||
// Function to add a new gantt with data and filename
|
||||
@@ -551,3 +578,33 @@ function toggleClientList() {
|
||||
|
||||
clientList.addClass("show");
|
||||
}
|
||||
|
||||
function loadWorkSchedule(workScheduleId) {
|
||||
const msg = {
|
||||
sourcePage: "gantt",
|
||||
operator: {
|
||||
type: "loadWorkSchedule",
|
||||
workScheduleId: workScheduleId,
|
||||
},
|
||||
};
|
||||
socket.emit("web_operator", msg);
|
||||
}
|
||||
|
||||
function removeWorkScheduleInfo(blenderId) {
|
||||
$("#work_schedules-"+ blenderId).empty();
|
||||
}
|
||||
|
||||
function getWorkScheduleData(blenderId) {
|
||||
const msg = {
|
||||
sourcePage: "gantt",
|
||||
operator: {
|
||||
type: "getWorkSchedules",
|
||||
},
|
||||
};
|
||||
|
||||
if (blenderId !== undefined) {
|
||||
msg.BlenderId = blenderId;
|
||||
}
|
||||
|
||||
socket.emit("web_operator", msg);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ function addTableElement(blenderId, csvData, filename) {
|
||||
}
|
||||
|
||||
// console.log(connectedClients[blenderId]);
|
||||
|
||||
const tableContainer = $("<div></div>")
|
||||
.addClass("table-container")
|
||||
.attr("id", "container-" + blenderId);
|
||||
@@ -206,26 +207,76 @@ function addTableElement(blenderId, csvData, filename) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
// create right click context menu for choosing column top calculation
|
||||
function createColumnContextMenu(column) {
|
||||
const field = column.field;
|
||||
const calculations = ["sum", "avg", "max", "min"];
|
||||
|
||||
// label is the item label in the menu
|
||||
// update column defination on clicking an item
|
||||
// specifically the topCalc and pass calc type to the topCalcFormatter function
|
||||
// uisng the topCalcFormatterParams field
|
||||
return calculations.map((calc) => ({
|
||||
label: `Show ${
|
||||
calc.charAt(0).toUpperCase() + calc.slice(1)
|
||||
} for ${field}`,
|
||||
action: (e, cell) =>
|
||||
cell
|
||||
.getColumn()
|
||||
.updateDefinition({ topCalc: calc, topCalcFormatterParams: calc }),
|
||||
}));
|
||||
}
|
||||
|
||||
function calcFormatter(cell, formatterParams, onRendered) {
|
||||
// formatterParams is anything in the topCalcFormatterParams field in the column definition
|
||||
// so we pass the calculation type to display what the calculation type is
|
||||
return `${formatterParams}: ${cell.getValue()}`;
|
||||
}
|
||||
|
||||
var containerHeight = $(".table-container").height();
|
||||
var h3Height = $(".table-container>h3").height();
|
||||
|
||||
var table = new Tabulator("#table-" + blenderId, {
|
||||
placeholder: "No data to display",
|
||||
height: "400px",
|
||||
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,
|
||||
// defines column defualts that are applied to all columns
|
||||
columnDefaults: {
|
||||
visible: true,
|
||||
headerFilter: true,
|
||||
topCalc: "sum",
|
||||
topCalcFormatter: calcFormatter,
|
||||
topCalcFormatterParams: "sum",
|
||||
},
|
||||
// defines a callback to edit the column definition generated by autoColumns
|
||||
// that cannot be set as default
|
||||
autoColumnsDefinitions: function (definitions) {
|
||||
menu = createHeaderMenu(definitions, table);
|
||||
headerMenu = createHeaderMenu(definitions, table);
|
||||
definitions.forEach((column) => {
|
||||
column.visible = true;
|
||||
column.headerMenu = menu;
|
||||
column.headerMenu = headerMenu;
|
||||
column.contextMenu = createColumnContextMenu(column);
|
||||
});
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
export class CostUI {
|
||||
constructor() {}
|
||||
|
||||
createButton() {
|
||||
console.log("Button created");
|
||||
}
|
||||
|
||||
createInput() {
|
||||
console.log("Input created");
|
||||
}
|
||||
|
||||
static isCostScheduleLoaded(id) {
|
||||
const existingTable = document.getElementById('cost-items-' + id);
|
||||
return existingTable !== null;
|
||||
}
|
||||
|
||||
static removeCostSchedule(id) {
|
||||
document.getElementById("cost-items-" + id).remove();
|
||||
}
|
||||
static createTable(id) {
|
||||
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
|
||||
|
||||
const table = document.createElement("table");
|
||||
table.id = 'cost-items-' + id;
|
||||
const tbody = document.createElement("tbody");
|
||||
tbody.setAttribute("id", "cost-items");
|
||||
|
||||
const columnHeaders = ["Name", "Quantity", "Unit", "Cost", "Total Cost", "Action"];
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
for (let i = 0; i < columnHeaders.length; i++) {
|
||||
const th = document.createElement("th");
|
||||
th.textContent = columnHeaders[i];
|
||||
th.style.position = "relative"; // Required for the resizer handle
|
||||
tr.appendChild(th);
|
||||
|
||||
// Add resizer handle
|
||||
if (i < columnHeaders.length - 1) { // No resizer for the last column
|
||||
const resizer = document.createElement("div");
|
||||
resizer.classList.add("resizer");
|
||||
th.appendChild(resizer);
|
||||
CostUI.addResizer(resizer);
|
||||
}
|
||||
}
|
||||
|
||||
tbody.appendChild(tr);
|
||||
table.appendChild(tbody);
|
||||
document.getElementById("cost-items").appendChild(table);
|
||||
|
||||
// Add CSS to set column widths, resizer styles, hover effect, and color scheme
|
||||
CostUI.addTableStyles(id);
|
||||
|
||||
// Create context menu
|
||||
CostUI.createContextMenu();
|
||||
|
||||
table.get_blender_id = function() {
|
||||
return this.getAttribute("id").split("-")[2];
|
||||
};
|
||||
|
||||
return [table, tbody];
|
||||
}
|
||||
|
||||
static addTableStyles(id) {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
#cost-items-${id} th:nth-child(1),
|
||||
#cost-items-${id} td:nth-child(1) {
|
||||
width: auto;
|
||||
}
|
||||
#cost-items-${id} th:not(:nth-child(1)),
|
||||
#cost-items-${id} td:not(:nth-child(1)) {
|
||||
width: 100px; /* Set a fixed width for other columns */
|
||||
}
|
||||
th {
|
||||
position: relative;
|
||||
}
|
||||
.resizer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
.context-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
.context-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.context-menu button:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
#cost-items tr:hover {
|
||||
background-color: #f0f0f0; /* Change this color to your desired hover color */
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
static createContextMenu() {
|
||||
// Create context menu
|
||||
const contextMenu = document.createElement("div");
|
||||
contextMenu.id = "context-menu";
|
||||
contextMenu.classList.add("context-menu");
|
||||
contextMenu.innerHTML = `
|
||||
<button id="edit-button">Edit</button>
|
||||
<button id="delete-button">Delete</button>
|
||||
<button id="duplicate-button">Duplicate</button>
|
||||
`;
|
||||
document.body.appendChild(contextMenu);
|
||||
|
||||
// Add event listeners for context menu
|
||||
document.addEventListener("contextmenu", function(event) {
|
||||
event.preventDefault();
|
||||
const targetRow = event.target.closest("tr");
|
||||
if (targetRow && targetRow.parentElement.id === "cost-items") {
|
||||
const contextMenu = document.getElementById("context-menu");
|
||||
contextMenu.style.display = "block";
|
||||
contextMenu.style.left = `${event.pageX}px`;
|
||||
contextMenu.style.top = `${event.pageY}px`;
|
||||
|
||||
// Store the target row in the context menu for later use
|
||||
contextMenu.targetRow = targetRow;
|
||||
} else {
|
||||
document.getElementById("context-menu").style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", function(event) {
|
||||
const contextMenu = document.getElementById("context-menu");
|
||||
if (!contextMenu.contains(event.target)) {
|
||||
contextMenu.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("edit-button").addEventListener("click", function() {
|
||||
const targetRow = document.getElementById("context-menu").targetRow;
|
||||
if (targetRow) {
|
||||
// Implement your edit action here
|
||||
console.log("Edit row:", targetRow.getAttribute("id"));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("delete-button").addEventListener("click", function() {
|
||||
const targetRow = document.getElementById("context-menu").targetRow;
|
||||
if (targetRow) {
|
||||
// Implement your delete action here
|
||||
console.log("Delete row:", targetRow.getAttribute("id"));
|
||||
targetRow.remove();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("duplicate-button").addEventListener("click", function() {
|
||||
const targetRow = document.getElementById("context-menu").targetRow;
|
||||
if (targetRow) {
|
||||
// Implement your duplicate action here
|
||||
console.log("Duplicate row:", targetRow.getAttribute("id"));
|
||||
const newRow = targetRow.cloneNode(true);
|
||||
targetRow.parentElement.appendChild(newRow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static addResizer(resizer) {
|
||||
let startX, startWidth, th;
|
||||
|
||||
resizer.addEventListener("mousedown", function(e) {
|
||||
th = e.target.parentElement;
|
||||
startX = e.pageX;
|
||||
startWidth = th.offsetWidth;
|
||||
document.addEventListener("mousemove", resizeColumn);
|
||||
document.addEventListener("mouseup", stopResize);
|
||||
});
|
||||
|
||||
function resizeColumn(e) {
|
||||
const newWidth = startWidth + (e.pageX - startX);
|
||||
th.style.width = newWidth + "px";
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
document.removeEventListener("mousemove", resizeColumn);
|
||||
document.removeEventListener("mouseup", stopResize);
|
||||
}
|
||||
}
|
||||
|
||||
static generateColorScheme(baseColor) {
|
||||
// This function generates a color scheme based on the base color
|
||||
// For simplicity, we'll just lighten the base color for each level
|
||||
const levels = 7; // Number of levels of nesting
|
||||
const colorScheme = [];
|
||||
for (let i = 0; i < levels; i++) {
|
||||
colorScheme.push(CostUI.lightenColor(baseColor, i * 7));
|
||||
}
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
static lightenColor(color, percent) {
|
||||
// This function lightens a color by a given percentage
|
||||
const num = parseInt(color.slice(1), 16),
|
||||
amt = Math.round(2.55 * percent),
|
||||
R = (num >> 16) + amt,
|
||||
G = (num >> 8 & 0x00FF) + amt,
|
||||
B = (num & 0x0000FF) + amt;
|
||||
return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`;
|
||||
}
|
||||
|
||||
static applyColorScheme(tableId, colorScheme) {
|
||||
const rows = document.querySelectorAll(`#${tableId} tbody tr`);
|
||||
rows.forEach((row, index) => {
|
||||
const level = index % colorScheme.length; // Assuming level is determined by index for simplicity
|
||||
row.style.backgroundColor = colorScheme[level];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static createColorPicker() {
|
||||
const colorPicker = document.createElement("input");
|
||||
colorPicker.type = "color";
|
||||
colorPicker.id = "color-picker";
|
||||
colorPicker.value = "#ff0000"; // Default color
|
||||
const colorText = document.createElement("p");
|
||||
colorText.textContent = "Select a color to change row color:";
|
||||
document.getElementById("UI").appendChild(colorText);
|
||||
document.getElementById("UI").appendChild(colorPicker);
|
||||
|
||||
colorPicker.addEventListener("input", function() {
|
||||
const baseColor = colorPicker.value;
|
||||
const colorScheme = CostUI.generateColorScheme(baseColor);
|
||||
CostUI.applyColorScheme("cost-items", colorScheme);
|
||||
});
|
||||
const colorScheme = CostUI.generateColorScheme("#000000");
|
||||
CostUI.applyColorScheme("cost-items", colorScheme);
|
||||
}
|
||||
|
||||
static createCostSchedule({ data, blenderID, title, callbacks = {} }) {
|
||||
const [table, tbody] = CostUI.createTable(blenderID);
|
||||
CostUI.createCostItem(data, tbody, 0, null, callbacks);
|
||||
CostUI.applyExpandedState();
|
||||
}
|
||||
|
||||
static createCostItem(data, container, nestingLevel = 0, parentID = null, callbacks = {}) {
|
||||
data.forEach(obj => {
|
||||
const row = CostUI.createRow(obj, nestingLevel, parentID, callbacks);
|
||||
container.appendChild(row);
|
||||
|
||||
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
|
||||
CostUI.createCostItem(obj.is_nested_by, container, nestingLevel + 1, obj.id, callbacks);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
|
||||
const row = document.createElement("tr");
|
||||
row.setAttribute("id", obj.id);
|
||||
row.setAttribute("parent-id", parentID);
|
||||
if (nestingLevel > 0) {
|
||||
row.classList.add("nested");
|
||||
row.classList.add(`level-${nestingLevel}`);
|
||||
}
|
||||
const expandButton = document.createElement("button");
|
||||
expandButton.classList.add("toggle-button");
|
||||
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
|
||||
expandButton.textContent = ">";
|
||||
} else {
|
||||
expandButton.style.visibility = "hidden";
|
||||
}
|
||||
//row.appendChild(expandButton);
|
||||
|
||||
expandButton.addEventListener("click", function() {
|
||||
CostUI.contractExpandRow.call(this, obj.id);
|
||||
});
|
||||
|
||||
const nameCell = document.createElement("td");
|
||||
const nameInput = document.createElement("input");
|
||||
nameInput.value = obj.name ? obj.name : "Unnamed";
|
||||
|
||||
nameInput.addEventListener("change", function() {
|
||||
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
||||
});
|
||||
|
||||
nameInput.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Enter") {
|
||||
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
||||
}
|
||||
});
|
||||
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
|
||||
nameCell.appendChild(expandButton);
|
||||
nameCell.appendChild(nameInput);
|
||||
row.appendChild(nameCell);
|
||||
|
||||
const totalCostQuantityCell = document.createElement("td");
|
||||
totalCostQuantityCell.textContent = obj.TotalCostQuantity;
|
||||
row.appendChild(totalCostQuantityCell);
|
||||
|
||||
const unitSymbolCell = document.createElement("td");
|
||||
unitSymbolCell.textContent = obj.UnitSymbol;
|
||||
row.appendChild(unitSymbolCell);
|
||||
|
||||
const totalAppliedValueCell = document.createElement("td");
|
||||
totalAppliedValueCell.textContent = obj.TotalAppliedValue;
|
||||
row.appendChild(totalAppliedValueCell);
|
||||
|
||||
const totalCostCell = document.createElement("td");
|
||||
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
|
||||
|
||||
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
|
||||
|
||||
row.appendChild(totalCostCell);
|
||||
|
||||
const divFlex = document.createElement("div");
|
||||
divFlex.classList.add("flex-container");
|
||||
const addButton = document.createElement("button");
|
||||
addButton.textContent = "+";
|
||||
addButton.classList.add("add-button");
|
||||
addButton.addEventListener("click", function(e) {
|
||||
e.stopPropagation();
|
||||
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
|
||||
});
|
||||
|
||||
const selectButton = document.createElement("button");
|
||||
selectButton.textContent = "Select";
|
||||
selectButton.addEventListener("click", function(e) {
|
||||
e.stopPropagation();
|
||||
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
|
||||
});
|
||||
|
||||
divFlex.appendChild(addButton);
|
||||
divFlex.appendChild(selectButton);
|
||||
|
||||
const flexContainerCell = document.createElement("td");
|
||||
flexContainerCell.appendChild(divFlex);
|
||||
row.appendChild(flexContainerCell);
|
||||
|
||||
row.get_id = function() {
|
||||
return this.getAttribute("id");
|
||||
};
|
||||
|
||||
row.get_parent = function() {
|
||||
const parentId = this.getAttribute("parent-id");
|
||||
return parentId ? document.getElementById(parentId) : null;
|
||||
};
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static hideNestedRows(parentId) {
|
||||
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
|
||||
rows.forEach(row => {
|
||||
row.classList.add("nested");
|
||||
const childId = row.getAttribute('id');
|
||||
if (childId) {
|
||||
CostUI.hideNestedRows(childId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static showNestedRows(parentId) {
|
||||
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
|
||||
rows.forEach(row => {
|
||||
row.classList.remove("nested");
|
||||
const childId = row.getAttribute('id');
|
||||
if (childId && CostUI.isRowExpanded(childId)) {
|
||||
CostUI.showNestedRows(childId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static contractExpandRow(id) {
|
||||
const rows = document.querySelectorAll(`[parent-id='${id}']`);
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isVisible = false;
|
||||
rows.forEach(row => {
|
||||
if (!row.classList.contains("nested")) {
|
||||
isVisible = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isVisible) {
|
||||
CostUI.hideNestedRows(id);
|
||||
this.textContent = ">";
|
||||
CostUI.updateExpandedState(id, false);
|
||||
} else {
|
||||
CostUI.showNestedRows(id);
|
||||
this.textContent = "^";
|
||||
CostUI.updateExpandedState(id, true);
|
||||
}
|
||||
}
|
||||
|
||||
static updateExpandedState(id, isExpanded) {
|
||||
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
|
||||
expandedState[id] = isExpanded;
|
||||
localStorage.setItem('expandedState', JSON.stringify(expandedState));
|
||||
}
|
||||
|
||||
static applyExpandedState() {
|
||||
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
|
||||
Object.keys(expandedState).forEach(id => {
|
||||
if (expandedState[id]) {
|
||||
CostUI.showNestedRows(id);
|
||||
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
|
||||
if (toggleButton) {
|
||||
toggleButton.textContent = "^";
|
||||
}
|
||||
} else {
|
||||
CostUI.hideNestedRows(id);
|
||||
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
|
||||
if (toggleButton) {
|
||||
toggleButton.textContent = ">";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static isRowExpanded(id) {
|
||||
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
|
||||
return expandedState[id] || false;
|
||||
}
|
||||
|
||||
static text(label) {
|
||||
const text = document.createElement("p");
|
||||
text.textContent = label;
|
||||
return text;
|
||||
}
|
||||
|
||||
static createCard(title, mainContainer, callback) {
|
||||
const card = document.createElement("div");
|
||||
card.classList.add("card");
|
||||
|
||||
const cardBody = document.createElement("div");
|
||||
cardBody.classList.add("card-body");
|
||||
|
||||
const cardTitle = document.createElement("h5");
|
||||
cardTitle.classList.add("card-title");
|
||||
cardTitle.textContent = title;
|
||||
|
||||
const cardButton = document.createElement("button");
|
||||
cardButton.classList.add("btn", "btn-primary");
|
||||
cardButton.textContent = "Load";
|
||||
cardButton.addEventListener("click", callback);
|
||||
|
||||
cardBody.appendChild(cardTitle);
|
||||
cardBody.appendChild(mainContainer);
|
||||
cardBody.appendChild(cardButton);
|
||||
card.appendChild(cardBody);
|
||||
|
||||
return card;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="blender">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlenderBIM Web UI</title>
|
||||
<link rel="stylesheet" href="/static/css/gantt.css" id="index-stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/components/card.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
id="tabulator-stylesheet"
|
||||
href="https://unpkg.com/tabulator-tables/dist/css/tabulator_site_dark.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
|
||||
></script>
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script type="module" defer src="./static/js/cost.js"></script>
|
||||
<style>
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.nested {
|
||||
display: none;
|
||||
}
|
||||
.expand-collapse {
|
||||
cursor: pointer;
|
||||
}
|
||||
.caret-cell {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
td {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.flex-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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="/costing" class="active"
|
||||
><i class="fa-solid fa-table"></i>Costing</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>
|
||||
<div id="connected-list-div">
|
||||
<button id="show-connected-button" onclick="toggleClientList()">
|
||||
Connected Blenders:
|
||||
<span id="blender-count">0</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div id="client-list"></div>
|
||||
</div>
|
||||
<div id="cost-schedules"></div>
|
||||
<div id="UI"></div>
|
||||
<div id="cost-items"></div>
|
||||
|
||||
<footer>
|
||||
<p>BlenderBIM Version: {{version}}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,273 @@
|
||||
<!-- 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>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<!-- 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"><<span class="hljs-name">script</span>></span><span class="actionscript">
|
||||
<span class="hljs-keyword">var</span> SOCKET_PORT = {{port}};
|
||||
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></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"><<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> /></span>
|
||||
<span class="hljs-tag"><<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>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></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">"<h3>"</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,12 @@
|
||||
<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="icon"
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||
@@ -53,6 +58,11 @@
|
||||
<li>
|
||||
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/costing"
|
||||
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sequencing"
|
||||
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
|
||||
@@ -149,7 +159,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p>BlenderBIM Version: {{version}}</p>
|
||||
<p>Bonsai Version: {{version}}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<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="icon"
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
|
||||
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
|
||||
<link
|
||||
@@ -22,7 +27,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script defer src="./static/js/gantt.js"></script>
|
||||
<script type="module" defer src="./static/js/gantt.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="no-print">
|
||||
@@ -35,6 +40,11 @@
|
||||
<li>
|
||||
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/costing"
|
||||
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sequencing" class="active"
|
||||
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
|
||||
@@ -71,9 +81,10 @@
|
||||
</button>
|
||||
<div id="client-list" class="no-print"></div>
|
||||
</div>
|
||||
<div id="work_schedules"></div>
|
||||
<div id="container"></div>
|
||||
<footer>
|
||||
<p>BlenderBIM Version: {{version}}</p>
|
||||
<p>Bonsai Version: {{version}}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<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="icon"
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
@@ -44,6 +49,11 @@
|
||||
><i class="fa-solid fa-table"></i> Schedules</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/costing"
|
||||
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sequencing"
|
||||
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
|
||||
@@ -78,7 +88,7 @@
|
||||
</div>
|
||||
<div id="container"></div>
|
||||
<footer>
|
||||
<p>BlenderBIM Version: {{version}}</p>
|
||||
<p>Bonsai Version: {{version}}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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
|
||||
@@ -116,17 +117,34 @@ class IfcStore:
|
||||
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
|
||||
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_path = Path(IfcStore.cache_path)
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
cache_preexists = cache_path.exists()
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_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)
|
||||
except:
|
||||
return
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
if cache_preexists:
|
||||
print(f"Successfully loaded existing cache: {cache_path.name}.")
|
||||
else:
|
||||
print("New cache was created.")
|
||||
except Exception as e:
|
||||
if cache_preexists:
|
||||
print(f"Failed to create a cache from existing file '{cache_path.name}': {str(e)}.")
|
||||
else:
|
||||
print(f"Failed to create a cache: {str(e)}.")
|
||||
# No point to trying again the same operation.
|
||||
return
|
||||
|
||||
os.remove(IfcStore.cache_path)
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
print("New cache was created.")
|
||||
except Exception as e:
|
||||
print(f"Failed to create a cache: {str(e)}.")
|
||||
return
|
||||
return IfcStore.cache
|
||||
|
||||
@@ -134,6 +152,8 @@ class IfcStore:
|
||||
def update_cache():
|
||||
if not IfcStore.cache:
|
||||
return
|
||||
assert IfcStore.cache_path
|
||||
assert IfcStore.file
|
||||
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
|
||||
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
|
||||
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
|
||||
@@ -228,7 +248,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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
@@ -433,9 +440,11 @@ class SelectExpressFile(bpy.types.Operator):
|
||||
class PurgeHdf5Cache(bpy.types.Operator):
|
||||
bl_idname = "bim.purge_hdf5_cache"
|
||||
bl_label = "Purge HDF5 Cache"
|
||||
bl_description = "Clean up HDF5 cache files except the ones that currently loaded"
|
||||
|
||||
def execute(self, context):
|
||||
core.purge_hdf5_cache(tool.Debug)
|
||||
self.report({"INFO"}, "HDF5 cache purged.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -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!")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ classes = (
|
||||
operator.SelectAllDrawings,
|
||||
operator.SelectAssignedProduct,
|
||||
operator.SelectDocIfcFile,
|
||||
operator.OpenDocumentationWebUi,
|
||||
prop.Variable,
|
||||
prop.Drawing,
|
||||
prop.Document,
|
||||
|
||||
@@ -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 += " - "
|
||||
|
||||
@@ -1463,7 +1463,7 @@ class ActivateModel(bpy.types.Operator):
|
||||
bl_idname = "bim.activate_model"
|
||||
bl_label = "Activate Model"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Activates the model view"
|
||||
bl_description = "Activate the model view, hide all annotations"
|
||||
|
||||
def execute(self, context):
|
||||
dprops = bpy.context.scene.DocProperties
|
||||
@@ -1471,10 +1471,23 @@ class ActivateModel(bpy.types.Operator):
|
||||
|
||||
CutDecorator.uninstall()
|
||||
|
||||
# save current visibility statuses
|
||||
# Preserve current visibility statuses for:
|
||||
# - non-ifc objects
|
||||
# - type product
|
||||
# - annotations (so we won't unhide other drawings)
|
||||
ifc_file = tool.Ifc.get()
|
||||
visibility_status: dict[bpy.types.Object, bool] = {}
|
||||
for obj in bpy.data.objects:
|
||||
visibility_status[obj] = obj.hide_get()
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
hide = obj.hide_get()
|
||||
elif element.is_a("IfcAnnotation"):
|
||||
hide = True
|
||||
elif element.is_a("IfcTypeProduct"):
|
||||
hide = obj.hide_get()
|
||||
else:
|
||||
continue
|
||||
visibility_status[obj] = hide
|
||||
|
||||
if not bpy.app.background:
|
||||
with context.temp_override(**tool.Blender.get_viewport_context()):
|
||||
@@ -1779,7 +1792,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.index:
|
||||
index = int(self.index)
|
||||
else:
|
||||
index = context.active_object.data.BIMCameraProperties.active_drawing_style_index
|
||||
index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index
|
||||
scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
|
||||
|
||||
bpy.ops.bim.save_drawing_styles_data()
|
||||
@@ -1875,7 +1888,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
def set_raster_style(self, context):
|
||||
def set_raster_style(self, context: bpy.types.Context) -> None:
|
||||
scene = context.scene # Do not remove. It is used in exec later
|
||||
space = self.get_view_3d(context) # Do not remove. It is used in exec later
|
||||
style = json.loads(self.drawing_style.raster_style)
|
||||
@@ -1889,7 +1902,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Differences in Blender versions mean result in failures here
|
||||
print(f"Failed to set shading style {path} to {value}")
|
||||
|
||||
def set_query(self, context):
|
||||
def set_query(self, context: bpy.types.Context) -> None:
|
||||
self.include_global_ids = []
|
||||
self.exclude_global_ids = []
|
||||
for ifc_file in context.scene.DocProperties.ifc_files:
|
||||
@@ -1908,7 +1921,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.drawing_style.exclude_query:
|
||||
self.parse_filter_query("EXCLUDE", context)
|
||||
|
||||
def parse_filter_query(self, mode, context):
|
||||
def parse_filter_query(self, mode: Literal["INCLUDE", "EXCLUDE"], context: bpy.types.Context) -> None:
|
||||
if mode == "INCLUDE":
|
||||
objects = context.scene.objects
|
||||
elif mode == "EXCLUDE":
|
||||
@@ -1927,7 +1940,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if global_id in self.exclude_global_ids:
|
||||
obj.hide_viewport = True # Note: this breaks alt-H
|
||||
|
||||
def get_view_3d(self, context):
|
||||
def get_view_3d(self, context: bpy.types.Context) -> bpy.types.Space:
|
||||
for area in context.screen.areas:
|
||||
if area.type != "VIEW_3D":
|
||||
continue
|
||||
@@ -1935,6 +1948,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if space.type != "VIEW_3D":
|
||||
continue
|
||||
return space
|
||||
assert False, "Space is not found."
|
||||
|
||||
|
||||
class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -2678,8 +2692,9 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
|
||||
filter_mode: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.active_object.data.BIMCameraProperties
|
||||
obj = bpy.context.scene.camera
|
||||
assert obj
|
||||
props = obj.data.BIMCameraProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
|
||||
if self.filter_mode == "INCLUDE":
|
||||
@@ -2857,3 +2872,16 @@ class ConvertSVGToDXF(bpy.types.Operator):
|
||||
|
||||
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenDocumentationWebUi(bpy.types.Operator):
|
||||
bl_idname = "bim.open_documentation_web_ui"
|
||||
bl_label = "Open Documentation Web UI"
|
||||
bl_description = "Open the documentation web UI page"
|
||||
|
||||
def execute(self, context):
|
||||
if not context.scene.WebProperties.is_connected:
|
||||
bpy.ops.bim.connect_websocket_server(page="documentation")
|
||||
else:
|
||||
bpy.ops.bim.bim.open_web_browser(page="documentation")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -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)
|
||||
@@ -253,8 +259,12 @@ def update_should_draw_decorations(self, context):
|
||||
continue
|
||||
tool.Drawing.update_text_value(obj)
|
||||
refresh_drawing_data()
|
||||
if bpy.app.background:
|
||||
return
|
||||
decoration.DecorationsHandler.install(context)
|
||||
else:
|
||||
if bpy.app.background:
|
||||
return
|
||||
decoration.DecorationsHandler.uninstall()
|
||||
|
||||
|
||||
@@ -400,6 +410,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
|
||||
@@ -524,9 +535,10 @@ def get_relating_type_id(self, context):
|
||||
|
||||
|
||||
def update_annotation_object_type(self, context):
|
||||
self.relating_type_id = "0"
|
||||
# changing enum doesn't trigger refresh by itself
|
||||
# Refresh enum items before changing property,
|
||||
# otherwise it might map to the wrong item.
|
||||
AnnotationData.is_loaded = False
|
||||
self.relating_type_id = "0"
|
||||
|
||||
|
||||
def update_sheet_data(self, context):
|
||||
|
||||
@@ -106,11 +106,7 @@ class BIM_PT_element_filters(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.scene.camera
|
||||
and context.active_object
|
||||
and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
)
|
||||
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
|
||||
|
||||
def draw(self, context):
|
||||
if not ElementFiltersData.is_loaded:
|
||||
@@ -160,17 +156,15 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.scene.camera
|
||||
and context.active_object
|
||||
and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
)
|
||||
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
camera = context.scene.camera
|
||||
assert camera
|
||||
dprops = context.scene.DocProperties
|
||||
props = context.active_object.data.BIMCameraProperties
|
||||
props = camera.data.BIMCameraProperties
|
||||
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
@@ -289,6 +283,9 @@ class BIM_PT_drawings(Panel):
|
||||
create_drawing_button = row.row(align=True)
|
||||
create_drawing_button.operator("bim.create_drawing", text="", icon="OUTPUT")
|
||||
create_drawing_button.enabled = active_drawing.ifc_definition_id > 0
|
||||
|
||||
# might need a different icon since the URL icon is already used by the open_drawing
|
||||
row.operator("bim.open_documentation_web_ui", icon="URL", text="")
|
||||
self.layout.template_list(
|
||||
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
@@ -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,20 @@ class WallPolylineDecorator:
|
||||
input_type = None
|
||||
angle_snap_mat = None
|
||||
angle_snap_loc = None
|
||||
use_default_container = False
|
||||
instructions = None
|
||||
snap_info = None
|
||||
|
||||
@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_on_screen_menu, (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 +346,37 @@ 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 set_instructions(cls, instructions):
|
||||
cls.instructions = instructions
|
||||
|
||||
@classmethod
|
||||
def set_snap_info(cls, snap_info):
|
||||
cls.snap_info = snap_info
|
||||
|
||||
@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 +391,30 @@ 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)
|
||||
angle = tool.Cad.angle_3_vectors(snap_vector, last_point, second_to_last_point, degrees=True)
|
||||
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 +423,94 @@ 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 +522,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 +542,90 @@ 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 = 1
|
||||
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 draw_on_screen_menu(self, context):
|
||||
region = context.region
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.font_id = 2
|
||||
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)
|
||||
|
||||
text_w, text_h = blf.dimensions(0, self.instructions)
|
||||
position = (region.width / 2) - (text_w / 2)
|
||||
blf.position(self.font_id, position, 10, 0)
|
||||
blf.draw(self.font_id, self.instructions)
|
||||
|
||||
text_w, text_h = blf.dimensions(0, self.snap_info)
|
||||
position = (region.width / 2) - (text_w / 2)
|
||||
blf.position(self.font_id, position, 30, 0)
|
||||
blf.draw(self.font_id, self.snap_info)
|
||||
|
||||
def __call__(self, context):
|
||||
|
||||
@@ -452,26 +648,28 @@ 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))]
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
# Create polyline with selected points
|
||||
polyline_data = context.scene.BIMModelProperties.polyline_point
|
||||
polyline_points = []
|
||||
polyline_edges = []
|
||||
@@ -482,20 +680,45 @@ class WallPolylineDecorator:
|
||||
for i in range(len(polyline_points) - 1):
|
||||
polyline_edges.append([i, i + 1])
|
||||
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
self.draw_batch("POINTS", polyline_points, decorator_color_selected)
|
||||
if len(polyline_points) > 1:
|
||||
self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges)
|
||||
|
||||
# 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:
|
||||
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)
|
||||
|
||||
# Line for angle axis snap
|
||||
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)
|
||||
|
||||
# Mouse points
|
||||
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))
|
||||
|
||||
if ref_point:
|
||||
self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0))
|
||||
|
||||
# Line between last polyline point and mouse
|
||||
edges = [[0, 1]]
|
||||
if polyline_points:
|
||||
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)
|
||||
|
||||
# Draw polyline with selected points
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
self.draw_batch("POINTS", polyline_points, decorator_color_selected)
|
||||
if len(polyline_points) > 1:
|
||||
self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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,62 @@ 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 = []
|
||||
self.instructions = """TAB: Cycle Input
|
||||
M: Modify Snap Point
|
||||
C: Close
|
||||
Backspace: Remove
|
||||
X Y: Axis
|
||||
Shift: Lock axis
|
||||
"""
|
||||
|
||||
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[self.input_type] = self.number_output
|
||||
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
|
||||
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
||||
else:
|
||||
self.input_panel[self.input_type] = self.number_output
|
||||
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 +360,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
|
||||
@@ -537,44 +393,52 @@ class DrawPolylineWall(bpy.types.Operator):
|
||||
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))
|
||||
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_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 == "X":
|
||||
tool.Snap.set_snap_axis_method("X")
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
if event.value == "PRESS" and event.type == "Y":
|
||||
tool.Snap.set_snap_axis_method("Y")
|
||||
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,21 +446,20 @@ 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:
|
||||
if event.value == "RELEASE" and event.type in {"D", "A"}:
|
||||
self.recalculate_inputs(context)
|
||||
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 +467,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 +475,46 @@ 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)
|
||||
PolylineDecorator.set_mouse_position(event)
|
||||
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
||||
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"}:
|
||||
tool.Snap.set_snap_axis_method(None)
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Snap.clear_polyline()
|
||||
tool.Blender.update_viewport()
|
||||
return {"CANCELLED"}
|
||||
@@ -659,14 +523,19 @@ 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_instructions(self.instructions)
|
||||
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)
|
||||
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_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"}
|
||||
@@ -797,10 +666,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 +718,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 +743,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:
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
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 +29,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,19 +240,36 @@ class CableTool(BimTool):
|
||||
BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(layout, text, hotkey, description):
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"C": "EVENT_CTRL",
|
||||
"S": "EVENT_SHIFT",
|
||||
}
|
||||
MODIFIERS = {
|
||||
"A": ("EVENT_ALT", "OPTION" if sys.platform == "Darwin" else "ALT"),
|
||||
"C": ("EVENT_CTRL", "CTRL"),
|
||||
"S": ("EVENT_SHIFT", "⇧"),
|
||||
}
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(layout, text, hotkey, description, ui_context=""):
|
||||
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
|
||||
|
||||
modifier_icon, modifier_str = MODIFIERS[modifier]
|
||||
|
||||
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=modifier_icon)
|
||||
row.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
hotkey_description = f"Hotkey: {modifier_str} {key}"
|
||||
if description:
|
||||
description += "\n\n"
|
||||
description += hotkey_description
|
||||
|
||||
op = row.operator("bim.hotkey", text=text)
|
||||
op.hotkey = hotkey
|
||||
op.description = description
|
||||
return op
|
||||
@@ -260,14 +297,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 +327,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 +360,73 @@ 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__, ui_context)
|
||||
|
||||
cls.layout.separator()
|
||||
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout,
|
||||
"Extend",
|
||||
"S_E",
|
||||
"Extends/reduces element to 3D cursor",
|
||||
ui_context,
|
||||
)
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout,
|
||||
"Butt",
|
||||
"S_T",
|
||||
"Intersects two non-parallel elements to a butt corner junction",
|
||||
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",
|
||||
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__, ui_context)
|
||||
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__, 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, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__, 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 +444,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 +460,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 +515,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 +532,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 +562,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__, ui_context)
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
add_layout_hotkey_operator(
|
||||
row,
|
||||
"Extend",
|
||||
"S_E",
|
||||
"Extends/reduces element to 3D cursor",
|
||||
ui_context,
|
||||
)
|
||||
add_layout_hotkey_operator(
|
||||
row,
|
||||
"Butt",
|
||||
"S_T",
|
||||
"Intersects two non-parallel elements to a butt corner junction",
|
||||
ui_context,
|
||||
)
|
||||
add_layout_hotkey_operator(
|
||||
row,
|
||||
"Mitre",
|
||||
"S_Y",
|
||||
"Intersects two non-parallel elements to a mitred corner junction",
|
||||
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__, ui_context)
|
||||
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__, ui_context)
|
||||
add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
|
||||
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__, 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 +830,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 +841,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,18 +851,10 @@ 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"
|
||||
bl_label = "Hotkey"
|
||||
bl_label = ""
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
@@ -843,7 +1141,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.props.z = self.z
|
||||
|
||||
def hotkey_S_P(self):
|
||||
bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
|
||||
mode = bpy.context.mode
|
||||
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
|
||||
if current_tool.idname == "bim.wall_tool":
|
||||
bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
|
||||
|
||||
def hotkey_S_L(self):
|
||||
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
|
||||
|
||||
@@ -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,262 @@ 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 = []
|
||||
self.instructions = """TAB: Cycle Input
|
||||
M: Modify Snap Point
|
||||
C: Close
|
||||
Backspace: Remove
|
||||
X Y Z: Axis
|
||||
S-(X Y Z): Plane
|
||||
Shift: Lock axis
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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_on_screen_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 == "X":
|
||||
tool.Snap.set_snap_axis_method("X")
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
if event.value == "PRESS" and event.type == "Y":
|
||||
tool.Snap.set_snap_axis_method("Y")
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
if event.value == "PRESS" and event.type == "Z":
|
||||
tool.Snap.set_snap_axis_method("Z")
|
||||
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 {"D", "A"} 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)
|
||||
PolylineDecorator.set_mouse_position(event)
|
||||
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
||||
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.cycle_snap_plane_method("YZ")
|
||||
tool.Snap.set_snap_axis_method(None)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
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.cycle_snap_plane_method("XZ")
|
||||
tool.Snap.set_snap_axis_method(None)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
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.cycle_snap_plane_method("XY")
|
||||
tool.Snap.set_snap_axis_method(None)
|
||||
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 {"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"}:
|
||||
tool.Snap.set_snap_plane_method(None)
|
||||
tool.Snap.set_snap_axis_method(None)
|
||||
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(None)
|
||||
tool.Snap.set_snap_axis_method(None)
|
||||
PolylineDecorator.set_instructions(self.instructions)
|
||||
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_on_screen_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"})
|
||||
|
||||
@@ -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):
|
||||
|
||||