mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f14b4c467d | |||
| 4a9cf433d1 | |||
| 866362c6ed | |||
| 0dc17a2141 | |||
| d55e7faf2a | |||
| 52762f828c | |||
| 1d0868eb18 | |||
| 9461c6369e | |||
| 0f3b570800 | |||
| 30cbfad871 | |||
| 262739f37a | |||
| 03fa850ce2 | |||
| 8d2c0d0785 | |||
| 742a9df311 | |||
| c95a0e8ed5 | |||
| a28478c193 | |||
| b521901c90 | |||
| 20f8737fb3 | |||
| f18c93c9ee | |||
| a461281b1e | |||
| 2d3d747d49 | |||
| d165cd2692 | |||
| 3f0e30245b | |||
| c111aafd3a | |||
| 465bd23d6f | |||
| c7d3e00dca | |||
| 72af88a4b3 | |||
| 73b76ba666 | |||
| e93a66be49 | |||
| a4dd1c3463 | |||
| 6d8e513f97 | |||
| baeaa073df | |||
| f07391181d | |||
| 722f71b3b3 | |||
| 76e4ec0069 | |||
| df18124bec | |||
| 370488cd27 | |||
| 4bbc463c8f | |||
| 4a11a117ad | |||
| 19be8107b3 | |||
| e1976d5bfd | |||
| 0fcc47508a | |||
| 76f0b57ff1 | |||
| 1d8ee406af | |||
| 05cb06a28e | |||
| 8a6eb1b60f | |||
| 37d4ee97e7 | |||
| ef5d1448da | |||
| eed42b8fac | |||
| 1f0ac8fdb9 | |||
| 1b1a1f143a | |||
| 9649c23a3b | |||
| 0ad5ace35f | |||
| f7fdb8cc81 | |||
| 41e64781d8 | |||
| 6ea7ee84a6 | |||
| e7b86ae5b0 | |||
| f6035609c9 | |||
| 76feeaccc9 | |||
| af02a6b3a6 | |||
| 832e68cb6f | |||
| a8268e8323 | |||
| 5a4ad69bf7 | |||
| 40f92d15c3 | |||
| ad8d02bfed | |||
| 5866994b84 | |||
| 2ec2ac0e5c | |||
| c7555652d7 | |||
| 5a7ce6d68b | |||
| ad8913a979 | |||
| 188b6a03a8 | |||
| 7f3860b025 | |||
| d08756d1b4 | |||
| 525ef2b77b | |||
| fca2bf7aa8 | |||
| ff82262490 | |||
| c82351476a | |||
| d4f32802aa | |||
| d7668c3e3f | |||
| 1445f91289 | |||
| 44e27f706a | |||
| 416ebde333 | |||
| 40dcd393c6 | |||
| 4fda916c2f | |||
| acb9953ce3 | |||
| 4154356288 | |||
| 54488bddbb | |||
| 8f21866248 | |||
| f3b63aae76 | |||
| 3f05bab4e5 | |||
| 8534333092 | |||
| 5f2b4c96a6 | |||
| 00836ec983 | |||
| fb78f75541 | |||
| 5948282ed0 | |||
| 47b81a6f89 | |||
| 8ae08cb90e | |||
| 7796856119 | |||
| f75045f0c5 | |||
| e52af2baad | |||
| da4ae4c2f2 | |||
| 28043a8e5d | |||
| 0e8fb44301 | |||
| b97dca5d47 | |||
| 207db459a7 | |||
| 026eebf39b | |||
| 5f5c0b41d5 | |||
| 3055dfa1a6 | |||
| b59bdb585b | |||
| ccd97c66ae | |||
| e6747b258c | |||
| 6aa6803490 | |||
| 72fbe3a6d9 | |||
| 68a602573b | |||
| 1e06dfcb76 | |||
| 3a0a912022 | |||
| b16fb7d917 | |||
| 94d2d32d18 | |||
| e743db27f4 | |||
| bb329affb8 | |||
| 6c4e4e5f91 | |||
| a31aa6053e | |||
| b90f146a68 | |||
| f67dca8de0 | |||
| a686d70908 | |||
| 38fe7fea65 | |||
| b26890f74b | |||
| 2c16247943 | |||
| 53fc7bce0c | |||
| f5db905223 | |||
| 3557b11e64 | |||
| c420402334 | |||
| 589c562cba | |||
| d3f2711fe4 | |||
| 8b4305360e | |||
| 1213bbdd73 | |||
| c3380daa19 | |||
| 7aa87907d7 | |||
| 8cfd6b1754 | |||
| fc10915d2b | |||
| c23c21cb88 | |||
| 260bc80a4b | |||
| d7cf803fdf | |||
| 4488044a68 | |||
| 21a4db7823 | |||
| d300454118 | |||
| 553003e664 | |||
| 524561a398 | |||
| 16f82683c8 | |||
| 4dbec59b9c | |||
| dcf69c46e5 |
@@ -16,7 +16,7 @@ jobs:
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils
|
||||
findutils xz
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
- name: Install aws cli
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils
|
||||
findutils xz
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
- name: Install aws cli
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
||||
|
||||
# Download Blender.
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.5/blender-4.5.0-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
|
||||
@@ -16,8 +16,6 @@ extend-exclude = '''
|
||||
reportInvalidTypeForm = false
|
||||
disableBytesTypePromotions = true
|
||||
reportUnnecessaryTypeIgnoreComment = true
|
||||
# Just to add a quick insert of `# pyright: ignore[xxx]` comments in Pylance.
|
||||
enableTypeIgnoreComments = false
|
||||
|
||||
# Define here general ruff settings,
|
||||
# then they will be inherited projects .toml files.
|
||||
|
||||
@@ -105,6 +105,8 @@ classes = [
|
||||
operator.BIM_OT_select_object,
|
||||
operator.BIM_OT_show_description,
|
||||
operator.BIM_OT_multiple_file_selector,
|
||||
operator.BIM_OT_attribute_add_subitem,
|
||||
operator.BIM_OT_attribute_remove_subitem,
|
||||
operator.ClippingPlaneCutWithCappings,
|
||||
operator.CloseBlendWarning,
|
||||
operator.CloseError,
|
||||
@@ -125,6 +127,7 @@ classes = [
|
||||
operator.ShowSystemInfo,
|
||||
prop.StrProperty,
|
||||
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
||||
operator.BIM_OT_attribute_search_values,
|
||||
prop.ObjProperty,
|
||||
prop.MultipleFileSelect,
|
||||
prop.Attribute,
|
||||
@@ -133,7 +136,6 @@ classes = [
|
||||
prop.BIMProperties,
|
||||
prop.IfcParameter,
|
||||
prop.PsetQto,
|
||||
prop.GlobalId,
|
||||
prop.BIMObjectProperties,
|
||||
prop.BIMCollectionProperties,
|
||||
prop.BIMMeshProperties,
|
||||
@@ -144,6 +146,7 @@ classes = [
|
||||
ui.BIM_UL_clipping_plane,
|
||||
ui.BIM_UL_generic,
|
||||
ui.BIM_UL_topics,
|
||||
ui.DocPreferences,
|
||||
ui.BIM_ADDON_preferences,
|
||||
# Tabs panel
|
||||
ui.BIM_PT_tabs,
|
||||
|
||||
@@ -30,7 +30,7 @@ from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
|
||||
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
|
||||
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
|
||||
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator, BoundingBoxDecorator
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator
|
||||
from mathutils import Vector
|
||||
from math import cos
|
||||
@@ -409,9 +409,13 @@ def load_post(scene):
|
||||
WallAxisDecorator.install(bpy.context)
|
||||
if model_props.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
if model_props.show_bounding_box:
|
||||
BoundingBoxDecorator.install(bpy.context)
|
||||
|
||||
if preferences.should_use_snap and (scene := bpy.context.scene):
|
||||
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
|
||||
scene.tool_settings.use_snap = True
|
||||
# Match default Bonsai snaps
|
||||
scene.tool_settings.snap_elements_base = {"EDGE", "EDGE_PERPENDICULAR", "VERTEX", "EDGE_MIDPOINT", "FACE"}
|
||||
|
||||
tool.Blender.sync_old_preferences()
|
||||
|
||||
@@ -27,6 +27,7 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
|
||||
import bonsai.tool as tool
|
||||
from types import EllipsisType
|
||||
from typing import Optional, Any, Union, TYPE_CHECKING
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
@@ -39,6 +40,9 @@ if TYPE_CHECKING:
|
||||
# - None - property should be imported by default workflow
|
||||
# - True - setting value for imported attribute should be skipped
|
||||
# - False - property should be skipped entirely from import
|
||||
# Second argument is optional,
|
||||
# because ImportCallback might be called for attributes that are not created by default
|
||||
# (e.g. IFC entity attributes).
|
||||
ImportCallback = Callable[[str, Optional[bonsai.bim.prop.Attribute], dict[str, Any]], Union[bool, None]]
|
||||
# ExportCallback return values:
|
||||
# - True - property should be skipped entirely from export
|
||||
@@ -52,30 +56,61 @@ def draw_attributes(
|
||||
copy_operator: Optional[str] = None,
|
||||
popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
|
||||
callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None,
|
||||
*,
|
||||
enable_search: Union[bool, EllipsisType] = ...,
|
||||
) -> None:
|
||||
"""Draw editable UI for prop.Attributes.
|
||||
|
||||
You can set attribute active in popup with `active_attribute`
|
||||
meaning you will be able to type into attribute's field without having to click
|
||||
on it first
|
||||
|
||||
:param enable_search: Add search button to string, integer, and float attributes
|
||||
Possible values:
|
||||
|
||||
- ``...`` (default value) -
|
||||
add search if possible. If it's not possible, there will be no warnings or errors.
|
||||
- ``True`` - always add search, if it's not possible it will result in errors.
|
||||
- ``False`` - never add search.
|
||||
|
||||
"""
|
||||
for attribute in props:
|
||||
row = layout.row(align=True)
|
||||
if attribute == popup_active_attribute:
|
||||
row.activate_init = True
|
||||
draw_attribute(attribute, row, copy_operator)
|
||||
draw_attribute(attribute, row, copy_operator, enable_search=enable_search)
|
||||
if callback:
|
||||
callback(attribute, row)
|
||||
|
||||
|
||||
def draw_attribute(
|
||||
attribute: bonsai.bim.prop.Attribute, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
|
||||
attribute: bonsai.bim.prop.Attribute,
|
||||
layout: bpy.types.UILayout,
|
||||
copy_operator: Optional[str] = None,
|
||||
enable_search: Union[bool, EllipsisType] = ...,
|
||||
) -> None:
|
||||
value_name = attribute.get_value_name(display_only=True)
|
||||
|
||||
if value_name == "enum_value":
|
||||
prop_with_search(layout, attribute, "enum_value", text=attribute.name)
|
||||
elif value_name == "filepath_value":
|
||||
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
|
||||
|
||||
elif value_name == "subitems_values":
|
||||
col = layout.column()
|
||||
layout = col.row(align=True)
|
||||
layout.label(text=f"{attribute.name}:")
|
||||
data_path = tool.Blender.get_full_data_path(attribute, value_name)
|
||||
for i, item in enumerate(attribute.subitems_values, 1):
|
||||
row = col.row(align=True)
|
||||
row.alignment = "EXPAND"
|
||||
row.prop(item, "name", text=f"# {i}")
|
||||
op = row.operator("bim.attribute_remove_subitem", text="", icon="X")
|
||||
op.data_path = data_path
|
||||
op.index = i - 1
|
||||
op = layout.operator("bim.attribute_add_subitem", icon="ADD", text="")
|
||||
op.data_path = data_path
|
||||
|
||||
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
|
||||
props = tool.Sequence.get_work_schedule_props()
|
||||
for item in props.durations_attributes:
|
||||
@@ -96,17 +131,34 @@ def draw_attribute(
|
||||
text=attribute.display_name,
|
||||
)
|
||||
|
||||
if attribute.is_uri:
|
||||
if attribute.special_type == "URI":
|
||||
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
|
||||
op.data_path = attribute.path_from_id("string_value")
|
||||
op.attribute_data_path = tool.Blender.get_full_data_path(attribute)
|
||||
elif attribute.special_type in ("DATE", "DATETIME"):
|
||||
op = layout.operator("bim.datepicker", text="", icon="TIME")
|
||||
op.target_prop = attribute.path_from_id("string_value")
|
||||
op.include_time = attribute.special_type == "DATETIME"
|
||||
|
||||
if attribute.data_type in ("string", "integer", "float") and (
|
||||
enable_search is True or (enable_search is ... and attribute.ifc_class)
|
||||
):
|
||||
op = layout.operator("bim.attribute_search_values", text="", icon="VIEWZOOM")
|
||||
op.attribute_name = attribute.name
|
||||
op.attribute_ifc_class = attribute.ifc_class
|
||||
op.data_path = tool.Blender.get_full_data_path(attribute, value_name)
|
||||
op.data_type = attribute.data_type
|
||||
|
||||
if attribute.is_optional:
|
||||
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
|
||||
if attribute.use_explorer_ui:
|
||||
op = layout.operator("bim.explorer_show_ui_popup", text="", icon="ZOOM_SELECTED")
|
||||
op.ifc_class = attribute.ifc_class
|
||||
op.attribute_name = attribute.name
|
||||
op.data_path = tool.Blender.get_full_data_path(attribute, value_name)
|
||||
if ifc_id := attribute.get_value():
|
||||
op.preselect_ifc_id = int(ifc_id)
|
||||
|
||||
if attribute.name == "GlobalId":
|
||||
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
|
||||
elif copy_operator:
|
||||
@@ -115,23 +167,14 @@ def draw_attribute(
|
||||
|
||||
|
||||
def import_attributes(
|
||||
ifc_class: str,
|
||||
props: bpy.types.bpy_prop_collection_idprop[Attribute],
|
||||
data: dict[str, Any],
|
||||
callback: Optional[ImportCallback] = None,
|
||||
) -> None:
|
||||
schema = tool.Ifc.schema()
|
||||
assert (entity := schema.declaration_by_name(ifc_class).as_entity())
|
||||
for attribute in entity.all_attributes():
|
||||
import_attribute(attribute, props, data, callback=callback)
|
||||
|
||||
|
||||
# A more elegant attribute importer signature, intended to supersede import_attributes
|
||||
def import_attributes2(
|
||||
element: Union[str, ifcopenshell.entity_instance],
|
||||
props: bpy.types.bpy_prop_collection_idprop[Attribute],
|
||||
callback: Optional[ImportCallback] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param element: Entity or IFC class string.
|
||||
"""
|
||||
info: dict[str, Any]
|
||||
if isinstance(element, str):
|
||||
assert (entity := tool.Ifc.schema().declaration_by_name(element).as_entity())
|
||||
attributes = entity.all_attributes()
|
||||
@@ -153,15 +196,19 @@ def import_attribute(
|
||||
) -> None:
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
|
||||
# Complex data types (aggregates and entities) are handled only by callback.
|
||||
if data_type == ("list", "string"):
|
||||
data_type = "list[string]"
|
||||
if isinstance(data_type, tuple) or data_type == "entity":
|
||||
callback(attribute.name(), None, data) if callback else None
|
||||
return
|
||||
new: bonsai.bim.prop.Attribute = props.add()
|
||||
|
||||
new = props.add()
|
||||
new.name = attribute.name()
|
||||
new.is_null = data[attribute.name()] is None
|
||||
new.is_optional = attribute.optional()
|
||||
new.data_type = data_type if isinstance(data_type, str) else ""
|
||||
new.ifc_class = data["type"]
|
||||
|
||||
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
|
||||
data_type = new.data_type # Allow callback to override data type.
|
||||
|
||||
@@ -171,8 +218,9 @@ def import_attribute(
|
||||
props.remove(len(props) - 1)
|
||||
elif data_type == "string":
|
||||
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
|
||||
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
|
||||
new.is_uri = True
|
||||
attribute_type = attribute.type_of_attribute()
|
||||
if attribute_type._is("IfcURIReference"):
|
||||
new.special_type = "URI"
|
||||
elif attribute.type_of_attribute()._is("IfcDate"):
|
||||
new.special_type = "DATE"
|
||||
elif attribute.type_of_attribute()._is("IfcDateTime"):
|
||||
@@ -206,6 +254,12 @@ def import_attribute(
|
||||
|
||||
if enum_value is not None:
|
||||
new.enum_value = enum_value
|
||||
elif data_type == "list[string]":
|
||||
value: Union[list[str], None] = data[attribute.name()]
|
||||
if value:
|
||||
for item in value:
|
||||
new.subitems_values.add().name = str(item).replace("\n", "\\n")
|
||||
|
||||
add_attribute_description(new, data)
|
||||
add_attribute_min_max(attribute, new)
|
||||
|
||||
@@ -273,13 +327,14 @@ def export_attributes(
|
||||
return attributes
|
||||
|
||||
|
||||
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_name: str) -> None:
|
||||
entity_id = attributes[attribute_name]
|
||||
if entity_id is None:
|
||||
# Maybe it was removed by now and enum is invalid.
|
||||
del attributes[attribute_name]
|
||||
else:
|
||||
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
|
||||
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_names: list[str]) -> None:
|
||||
for attribute_name in attribute_names:
|
||||
entity_id = attributes[attribute_name]
|
||||
if entity_id is None:
|
||||
# Maybe it was removed by now and enum is invalid.
|
||||
del attributes[attribute_name]
|
||||
else:
|
||||
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
|
||||
|
||||
|
||||
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
|
||||
|
||||
@@ -105,7 +105,7 @@ class IfcStore:
|
||||
IfcStore.session_files = {}
|
||||
|
||||
@staticmethod
|
||||
def get_file():
|
||||
def get_file() -> ifcopenshell.file | None:
|
||||
if IfcStore.file is None:
|
||||
props = tool.Blender.get_bim_props()
|
||||
IfcStore.set_path(props.ifc_file)
|
||||
@@ -117,20 +117,20 @@ class IfcStore:
|
||||
return IfcStore.file
|
||||
|
||||
@staticmethod
|
||||
def set_path(value):
|
||||
def set_path(value: str) -> None:
|
||||
IfcStore.path = value
|
||||
# Interpret relative paths as relative to .blend file.
|
||||
if IfcStore.path and not os.path.isabs(IfcStore.path):
|
||||
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
|
||||
|
||||
@staticmethod
|
||||
def get_cache():
|
||||
def get_cache() -> ifcopenshell.geom.serializers.hdf5 | None:
|
||||
if IfcStore.cache is None and IfcStore.path:
|
||||
props = tool.Blender.get_bim_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
|
||||
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
|
||||
os.makedirs(props.cache_dir, exist_ok=True)
|
||||
IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
|
||||
os.makedirs(prefs.cache_dir, exist_ok=True)
|
||||
IfcStore.cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
|
||||
cache_path = Path(IfcStore.cache_path)
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
@@ -163,15 +163,15 @@ class IfcStore:
|
||||
return IfcStore.cache
|
||||
|
||||
@staticmethod
|
||||
def update_cache():
|
||||
def update_cache() -> None:
|
||||
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()
|
||||
props = tool.Blender.get_bim_props()
|
||||
new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
new_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
|
||||
IfcStore.cache = None
|
||||
try:
|
||||
shutil.move(IfcStore.cache_path, new_cache_path)
|
||||
@@ -183,11 +183,11 @@ class IfcStore:
|
||||
IfcStore.get_cache()
|
||||
|
||||
@staticmethod
|
||||
def load_file(path) -> None:
|
||||
def load_file(path: str) -> None:
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
extension = path.split(".")[-1]
|
||||
props = tool.Project.get_project_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if extension.lower() == "ifczip":
|
||||
with tempfile.TemporaryDirectory() as unzipped_path:
|
||||
with zipfile.ZipFile(path, "r") as zip_ref:
|
||||
@@ -197,7 +197,7 @@ class IfcStore:
|
||||
return
|
||||
elif extension.lower() == "ifcxml":
|
||||
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
|
||||
elif props.should_stream:
|
||||
elif prefs.should_stream:
|
||||
IfcStore.file = ifcopenshell.open(path, should_stream=True)
|
||||
else:
|
||||
IfcStore.file = ifcopenshell.open(path)
|
||||
|
||||
@@ -231,7 +231,8 @@ class IfcImporter:
|
||||
self.progress = 0
|
||||
|
||||
self.material_creator = MaterialCreator(ifc_import_settings, self)
|
||||
classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
classes_to_wireframe_str = prefs.doc.classes_to_wireframe
|
||||
self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")]
|
||||
|
||||
def profile_code(self, message: str) -> None:
|
||||
@@ -662,7 +663,7 @@ class IfcImporter:
|
||||
settings: Optional[ifcopenshell.geom.main.settings] = None,
|
||||
) -> set[ifcopenshell.entity_instance]:
|
||||
checkpoint = time.time()
|
||||
results = set()
|
||||
results: set[ifcopenshell.entity_instance] = set()
|
||||
if not products:
|
||||
return results
|
||||
|
||||
|
||||
@@ -225,16 +225,25 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.select_aggregate"
|
||||
bl_label = "Select Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
obj: bpy.props.StringProperty()
|
||||
select_parts: bpy.props.BoolProperty(default=False)
|
||||
one_level_deep: bpy.props.BoolProperty(
|
||||
name="One Level Deep", description="Select only immediate children, not recursively", default=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.select_parts:
|
||||
return "Select Aggregate and Parts"
|
||||
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep"
|
||||
else:
|
||||
return "Select Aggregate"
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.ctrl:
|
||||
self.one_level_deep = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
all_parts = []
|
||||
for obj in context.selected_objects:
|
||||
@@ -250,14 +259,27 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
obj.select_set(False)
|
||||
|
||||
if self.select_parts:
|
||||
all_objs = []
|
||||
selected_parts = []
|
||||
|
||||
for part in all_parts:
|
||||
if part.IsDecomposedBy:
|
||||
for subpart in part.IsDecomposedBy[0].RelatedObjects:
|
||||
all_parts.append(subpart)
|
||||
all_objs.append(part)
|
||||
for rel in part.IsDecomposedBy:
|
||||
for subpart in rel.RelatedObjects:
|
||||
selected_parts.append(subpart)
|
||||
|
||||
for element in all_objs:
|
||||
# If not limited to one level, traverse deeper
|
||||
if not self.one_level_deep:
|
||||
|
||||
def add_descendants(elem):
|
||||
if elem.IsDecomposedBy:
|
||||
for rel in elem.IsDecomposedBy:
|
||||
for deeper in rel.RelatedObjects:
|
||||
selected_parts.append(deeper)
|
||||
add_descendants(deeper)
|
||||
|
||||
add_descendants(subpart)
|
||||
|
||||
for element in set(selected_parts + all_parts):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
@@ -265,9 +287,11 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
else:
|
||||
for aggregate_element in all_parts:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate_element)
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
if aggregate_obj:
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
|
||||
self.one_level_deep = False # <-- forcibly reset
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
import os
|
||||
|
||||
import ifcopenshell.api.alignment
|
||||
import ifcopenshell.api.alignment.add_stationing_to_alignment
|
||||
|
||||
import bpy
|
||||
import json
|
||||
@@ -62,9 +61,7 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
start = time.time()
|
||||
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
|
||||
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
|
||||
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
|
||||
alignment = ifcopenshell.api.alignment.create_from_csv(self.file, self.filepath)
|
||||
|
||||
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
|
||||
sites = self.file.by_type("IfcSite")
|
||||
|
||||
@@ -25,13 +25,23 @@ classes = (
|
||||
operator.EditAttributes,
|
||||
operator.GenerateGlobalId,
|
||||
operator.CopyAttributeToSelection,
|
||||
operator.ExplorerAddEntity,
|
||||
operator.ExplorerEnableEditingEntity,
|
||||
operator.ExplorerDisableEditingEntity,
|
||||
operator.ExplorerEditEntity,
|
||||
operator.ExplorerShowUIPopup,
|
||||
prop.BIMAttributeProperties,
|
||||
prop.ExplorerEntity,
|
||||
prop.BIMExplorerProperties,
|
||||
ui.BIM_PT_object_attributes,
|
||||
ui.BIM_PT_explorer,
|
||||
ui.BIM_UL_explorer,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
|
||||
bpy.types.Scene.BIMExplorerProperties = bpy.props.PointerProperty(type=prop.BIMExplorerProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
@@ -26,7 +26,11 @@ import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.attribute as core
|
||||
import bonsai.core.spatial
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Union, Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import Attribute
|
||||
import bpy.stub_internal.rna_enums as rna_enums
|
||||
|
||||
|
||||
def get_objs_for_operation(
|
||||
@@ -75,7 +79,9 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
||||
None,
|
||||
)
|
||||
|
||||
def callback(name, prop, data):
|
||||
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
|
||||
|
||||
def callback(name: str, prop: Union["Attribute", None], data: dict[str, Any]) -> None | Literal[True]:
|
||||
if name in ("RefLatitude", "RefLongitude"):
|
||||
new = props.attributes.add()
|
||||
new.name = name
|
||||
@@ -89,8 +95,19 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
||||
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
|
||||
props.attributes.remove(len(props.attributes) - 1)
|
||||
return True
|
||||
if lookup_attrs and (name in lookup_attrs):
|
||||
new = props.attributes.add()
|
||||
new.name = name
|
||||
new.ifc_class = data["type"]
|
||||
new.data_type = "enum"
|
||||
new.is_optional = True
|
||||
new.enum_items_dynamic = lookup_attrs[name]
|
||||
new.use_explorer_ui = True
|
||||
value: Union[ifcopenshell.entity_instance, None] = data[name]
|
||||
if value is not None:
|
||||
new.enum_value = str(value.id())
|
||||
|
||||
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
|
||||
bonsai.bim.helper.import_attributes(element, props.attributes, callback=callback)
|
||||
props.is_editing_attributes = True
|
||||
|
||||
def execute(self, context):
|
||||
@@ -107,7 +124,8 @@ class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
||||
|
||||
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
props.is_editing_attributes = False
|
||||
props.attributes.clear()
|
||||
props.property_unset("is_editing_attributes")
|
||||
|
||||
def execute(self, context):
|
||||
for obj in get_objs_for_operation(self, context):
|
||||
@@ -127,7 +145,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not obj or not (element := tool.Ifc.get_entity(obj)):
|
||||
return
|
||||
|
||||
def callback(attributes, prop):
|
||||
def callback(attributes: dict[str, Any], prop: "Attribute") -> None | Literal[True]:
|
||||
if prop.name in ("RefLatitude", "RefLongitude"):
|
||||
if not prop.is_null:
|
||||
try:
|
||||
@@ -138,6 +156,9 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
|
||||
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
|
||||
if lookup_attrs:
|
||||
bonsai.bim.helper.process_exported_entity_attribute(attributes, list(lookup_attrs))
|
||||
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
|
||||
|
||||
tool.Root.set_object_name(obj, element)
|
||||
@@ -204,3 +225,139 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
|
||||
)
|
||||
self.report({"INFO"}, f"Attribute was successfully copied to {total} elements.")
|
||||
|
||||
|
||||
class ExplorerAddEntity(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.explorer_add_entity"
|
||||
bl_label = "Add Entity"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context) -> None:
|
||||
props = tool.Attribute.get_explorer_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
entity = ifc_file.create_entity(props.ifc_class)
|
||||
tool.Attribute.refresh_uilist_entities()
|
||||
tool.Attribute.enable_editing_entity(entity)
|
||||
tool.Attribute.import_entity_attributes(entity)
|
||||
|
||||
|
||||
class ExplorerEnableEditingEntity(bpy.types.Operator):
|
||||
bl_idname = "bim.explorer_enable_editing_entity"
|
||||
bl_label = "Enable Editing Entity"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
props = tool.Attribute.get_explorer_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
assert (active_entity := props.active_entity)
|
||||
entity = ifc_file.by_id(active_entity.ifc_definition_id)
|
||||
|
||||
tool.Attribute.disable_editing_entity()
|
||||
tool.Attribute.enable_editing_entity(entity)
|
||||
tool.Attribute.import_entity_attributes(entity)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExplorerDisableEditingEntity(bpy.types.Operator):
|
||||
bl_idname = "bim.explorer_disable_editing_entity"
|
||||
bl_label = "Disable Editing Entity"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
tool.Attribute.disable_editing_entity()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExplorerEditEntity(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.explorer_edit_entity"
|
||||
bl_label = "Edit Entity"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Attribute.get_explorer_props()
|
||||
entity = ifc_file.by_id(props.editing_entity_id)
|
||||
|
||||
attrs = tool.Attribute.export_entity_attributes()
|
||||
for attr, value in attrs.items():
|
||||
setattr(entity, attr, value)
|
||||
tool.Attribute.refresh_uilist_entities()
|
||||
tool.Attribute.disable_editing_entity()
|
||||
|
||||
|
||||
class ExplorerShowUIPopup(bpy.types.Operator):
|
||||
bl_idname = "bim.explorer_show_ui_popup"
|
||||
bl_label = "Show Explorer UI"
|
||||
bl_description = "Show Explorer UI to select element as attribute value or edit it."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""Element IFC class."""
|
||||
attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""IFC class attribute name."""
|
||||
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""Full data path"""
|
||||
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
||||
"""IFC id to preselect in the popup."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ifc_class: str
|
||||
attribute_name: str
|
||||
data_path: str
|
||||
preselect_ifc_id: int
|
||||
|
||||
def invoke(self, context, event) -> "set[rna_enums.OperatorReturnItems]":
|
||||
assert context.window_manager
|
||||
assert self.ifc_class and self.attribute_name and self.data_path
|
||||
|
||||
props = tool.Attribute.get_explorer_props()
|
||||
props.is_loaded = True
|
||||
props.ifc_class = self.get_attribute_type()
|
||||
|
||||
if self.preselect_ifc_id:
|
||||
props.active_entity_index = next(
|
||||
i for i, e in enumerate(props.entities) if e.ifc_definition_id == self.preselect_ifc_id
|
||||
)
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def get_attribute_type(self) -> str:
|
||||
schema = tool.Ifc.schema()
|
||||
entity = schema.declaration_by_name(self.ifc_class).as_entity()
|
||||
assert entity
|
||||
i = entity.attribute_index(self.attribute_name)
|
||||
attr = entity.all_attributes()[i]
|
||||
named_type = attr.type_of_attribute().as_named_type()
|
||||
assert named_type
|
||||
declared = named_type.declared_type()
|
||||
return declared.name()
|
||||
|
||||
def draw(self, context) -> None:
|
||||
from bonsai.bim.module.attribute.ui import BIM_PT_explorer
|
||||
|
||||
BIM_PT_explorer.draw(self, context, is_popup=True)
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
props = tool.Attribute.get_explorer_props()
|
||||
active_entity = props.active_entity
|
||||
if active_entity is None:
|
||||
self.report({"WARNING"}, "No entity selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
# Apply pending changes for convenience.
|
||||
if props.editing_entity_id:
|
||||
if props.editing_entity_id == active_entity.ifc_definition_id:
|
||||
bpy.ops.bim.explorer_edit_entity()
|
||||
else:
|
||||
bpy.ops.bim.explorer_disable_editing_entity()
|
||||
|
||||
# Very important to do it after changes applied, otherwise enum might update
|
||||
# and index will be pointing to a different element.
|
||||
exec(f"{self.data_path} = '{active_entity.ifc_definition_id}'")
|
||||
return {"FINISHED"}
|
||||
|
||||
def cancel(self, context: bpy.types.Context) -> None:
|
||||
props = tool.Attribute.get_explorer_props()
|
||||
if props.editing_entity_id:
|
||||
bpy.ops.bim.explorer_disable_editing_entity()
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -29,7 +30,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
||||
class BIMAttributeProperties(PropertyGroup):
|
||||
@@ -39,3 +40,62 @@ class BIMAttributeProperties(PropertyGroup):
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
is_editing_attributes: bool
|
||||
|
||||
|
||||
class ExplorerEntity(PropertyGroup):
|
||||
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
class BIMExplorerProperties(PropertyGroup):
|
||||
def update_is_loaded(self, context: object) -> None:
|
||||
if self.is_loaded:
|
||||
# Trigger refresh.
|
||||
self.ifc_class = self.ifc_class
|
||||
else:
|
||||
self.property_unset("is_loaded")
|
||||
self.property_unset("ifc_class")
|
||||
self.entities.clear()
|
||||
self.property_unset("active_entity_index")
|
||||
self.property_unset("editing_entity_id")
|
||||
self.entity_attributes.clear()
|
||||
|
||||
is_loaded: BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Toggle Explorer UI",
|
||||
update=update_is_loaded,
|
||||
)
|
||||
|
||||
def get_ifc_class(self, context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
# TODO: Add more entities.
|
||||
classes = [
|
||||
"IfcPostalAddress",
|
||||
"IfcTelecomAddress",
|
||||
]
|
||||
return [(c, c, "") for c in classes]
|
||||
|
||||
def update_ifc_class(self, context: object) -> None:
|
||||
tool.Attribute.refresh_uilist_entities()
|
||||
|
||||
ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="IFC Class To Search",
|
||||
items=get_ifc_class,
|
||||
update=update_ifc_class,
|
||||
)
|
||||
entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration]
|
||||
active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_loaded: bool
|
||||
ifc_class: str
|
||||
entities: bpy.types.bpy_prop_collection_idprop[ExplorerEntity]
|
||||
active_entity_index: int
|
||||
editing_entity_id: int
|
||||
entity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
|
||||
@property
|
||||
def active_entity(self) -> Union[ExplorerEntity, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.entities, self.active_entity_index)
|
||||
|
||||
@@ -16,11 +16,16 @@
|
||||
# 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 bonsai.bim.helper
|
||||
import bpy.types
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.module.attribute.data import AttributesData
|
||||
import bonsai.tool as tool
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.attribute.prop import BIMExplorerProperties, ExplorerEntity
|
||||
|
||||
|
||||
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
|
||||
@@ -33,7 +38,9 @@ def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes)
|
||||
row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
|
||||
row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
|
||||
|
||||
bonsai.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
|
||||
bonsai.bim.helper.draw_attributes(
|
||||
props.attributes, layout, copy_operator="bim.copy_attribute_to_selection", enable_search=True
|
||||
)
|
||||
else:
|
||||
row = layout.row()
|
||||
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
|
||||
@@ -66,3 +73,73 @@ class BIM_PT_object_attributes(Panel):
|
||||
if not AttributesData.is_loaded:
|
||||
AttributesData.load()
|
||||
draw_ui(context, self.layout, AttributesData.data["attributes"])
|
||||
|
||||
|
||||
class BIM_PT_explorer(Panel):
|
||||
bl_label = "Explorer"
|
||||
bl_idname = "BIM_PT_explorer"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
def draw(self, context, *, is_popup=False):
|
||||
assert (layout := self.layout)
|
||||
props = tool.Attribute.get_explorer_props()
|
||||
|
||||
if is_popup:
|
||||
layout.label(text=props.ifc_class)
|
||||
else:
|
||||
if not props.is_loaded:
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Explorer UI is not Loaded.")
|
||||
row.prop(props, "is_loaded", text="", icon="IMPORT")
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "ifc_class", text="")
|
||||
row.prop(props, "is_loaded", text="", icon="CANCEL")
|
||||
|
||||
active_entity = props.active_entity
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{len(props.entities)} entities found")
|
||||
row.operator("bim.explorer_add_entity", text="", icon="ADD")
|
||||
|
||||
if active_entity:
|
||||
if active_entity.ifc_definition_id == props.editing_entity_id:
|
||||
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
|
||||
else:
|
||||
row.operator("bim.explorer_enable_editing_entity", icon="GREASEPENCIL", text="")
|
||||
# TODO: 'Remove' button?
|
||||
|
||||
layout.template_list("BIM_UL_explorer", "", props, "entities", props, "active_entity_index")
|
||||
|
||||
if props.editing_entity_id:
|
||||
box = self.layout.box()
|
||||
# In popup we accept edits automatically for the convenience.
|
||||
# Othrewise it seems very unintuitive, when you need to click confirmation twice.
|
||||
if not is_popup:
|
||||
row = box.row(align=True)
|
||||
row.operator("bim.explorer_edit_entity", icon="CHECKMARK")
|
||||
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
|
||||
bonsai.bim.helper.draw_attributes(props.entity_attributes, box, enable_search=True)
|
||||
|
||||
|
||||
class BIM_UL_explorer(bpy.types.UIList):
|
||||
def draw_item(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: BIMExplorerProperties,
|
||||
item: ExplorerEntity,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
) -> None:
|
||||
row = layout.row(align=True)
|
||||
|
||||
if item.ifc_definition_id == data.editing_entity_id:
|
||||
row.label(text=item.name, icon="GREASEPENCIL")
|
||||
else:
|
||||
row.label(text=item.name)
|
||||
|
||||
@@ -1208,9 +1208,11 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
self.file = tool.Ifc.get()
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
assert context.scene
|
||||
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
assert blender_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
if self.viewpoint_guid:
|
||||
viewpoint_guid = self.viewpoint_guid
|
||||
@@ -1234,6 +1236,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
cam_height = context.scene.render.resolution_y
|
||||
cam_aspect = cam_width / cam_height
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Camera)
|
||||
obj.data.background_images.clear()
|
||||
if viewpoint.snapshot:
|
||||
obj.data.show_background_images = True
|
||||
@@ -1273,7 +1276,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
if tool.Bcf.get_viewpoint_bitmaps(viewpoint):
|
||||
self.create_bitmaps(bcfxml, viewpoint, topic)
|
||||
|
||||
self.setup_camera(viewpoint, obj, cam_aspect, context)
|
||||
self.setup_camera(viewpoint, obj, cam_aspect, context, cam_height, cam_width)
|
||||
return {"FINISHED"}
|
||||
|
||||
def setup_camera(
|
||||
@@ -1282,6 +1285,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
obj: bpy.types.Object,
|
||||
cam_aspect: float,
|
||||
context: bpy.types.Context,
|
||||
cam_height: float,
|
||||
cam_width: float,
|
||||
) -> None:
|
||||
assert isinstance(obj.data, bpy.types.Camera)
|
||||
if viewpoint.visualization_info.orthogonal_camera:
|
||||
|
||||
@@ -35,7 +35,7 @@ from bpy.props import (
|
||||
from typing import Union, TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
def get_active_dictionary(self, context):
|
||||
def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not BSDDData.is_loaded:
|
||||
BSDDData.load()
|
||||
return BSDDData.data["active_dictionary"]
|
||||
@@ -158,15 +158,6 @@ class BIMBSDDProperties(PropertyGroup):
|
||||
description="Whether to display and assign only properties from IFC dictionary",
|
||||
default=False,
|
||||
)
|
||||
load_preview_dictionaries: BoolProperty(
|
||||
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
|
||||
)
|
||||
load_inactive_dictionaries: BoolProperty(
|
||||
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
|
||||
)
|
||||
load_test_dictionaries: BoolProperty(
|
||||
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
|
||||
)
|
||||
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -186,9 +177,6 @@ class BIMBSDDProperties(PropertyGroup):
|
||||
keyword: str
|
||||
should_filter_ifc_class: bool
|
||||
use_only_ifc_properties: bool
|
||||
load_preview_dictionaries: bool
|
||||
load_inactive_dictionaries: bool
|
||||
load_test_dictionaries: bool
|
||||
classification_psets: bpy.types.bpy_prop_collection_idprop[BSDDPset]
|
||||
|
||||
@property
|
||||
|
||||
@@ -33,7 +33,7 @@ def refresh():
|
||||
|
||||
|
||||
class ClassificationsData:
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
@@ -94,7 +94,7 @@ class ReferencesData:
|
||||
|
||||
|
||||
class ClassificationReferencesData(ReferencesData):
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
@@ -118,7 +118,7 @@ class ClassificationReferencesData(ReferencesData):
|
||||
|
||||
|
||||
class MaterialClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
@@ -134,8 +134,7 @@ class MaterialClassificationsData(ReferencesData):
|
||||
results = []
|
||||
|
||||
props = tool.Material.get_material_props()
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
material = props.materials[props.active_material_index]
|
||||
if material := props.active_material:
|
||||
if material.ifc_definition_id:
|
||||
element = tool.Ifc.get().by_id(material.ifc_definition_id)
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
@@ -146,7 +145,7 @@ class MaterialClassificationsData(ReferencesData):
|
||||
|
||||
|
||||
class CostClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -146,7 +146,7 @@ class EnableAddingManualClassification(bpy.types.Operator):
|
||||
props.is_adding = True
|
||||
props.active_classification_id = 0
|
||||
props.classification_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2("IfcClassification", props.classification_attributes)
|
||||
bonsai.bim.helper.import_attributes("IfcClassification", props.classification_attributes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ class EnableAddingManualClassificationReference(bpy.types.Operator):
|
||||
props = tool.Classification.get_classification_reference_props()
|
||||
props.is_adding = True
|
||||
props.reference_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
|
||||
bonsai.bim.helper.import_attributes("IfcClassificationReference", props.reference_attributes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ class EnableEditingClassification(bpy.types.Operator):
|
||||
|
||||
props = tool.Classification.get_classification_props()
|
||||
props.classification_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(
|
||||
bonsai.bim.helper.import_attributes(
|
||||
tool.Ifc.get().by_id(self.classification), props.classification_attributes, callback
|
||||
)
|
||||
props.active_classification_id = self.classification
|
||||
@@ -274,7 +274,7 @@ class EnableEditingClassificationReference(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Classification.get_classification_reference_props()
|
||||
props.reference_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
|
||||
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
|
||||
props.active_reference_id = self.reference
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from bonsai.bim.module.classification.data import (
|
||||
MaterialClassificationsData,
|
||||
CostClassificationsData,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.classification.prop import BIMClassificationProperties, ClassificationReference
|
||||
@@ -113,9 +113,15 @@ class BIM_PT_classifications(Panel):
|
||||
|
||||
class ReferenceUI:
|
||||
layout: bpy.types.UILayout
|
||||
data: type[
|
||||
Union[
|
||||
ClassificationReferencesData,
|
||||
MaterialClassificationsData,
|
||||
CostClassificationsData,
|
||||
]
|
||||
]
|
||||
|
||||
def draw_ui(self, context):
|
||||
obj = context.active_object
|
||||
def draw_ui(self, context) -> None:
|
||||
self.sprops = tool.Classification.get_classification_props()
|
||||
self.bprops = tool.Bsdd.get_bsdd_props()
|
||||
self.props = tool.Classification.get_classification_reference_props()
|
||||
@@ -133,7 +139,7 @@ class ReferenceUI:
|
||||
else:
|
||||
self.draw_reference_ui(reference)
|
||||
|
||||
def draw_add_ui(self, context):
|
||||
def draw_add_ui(self, context) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Source", icon="OUTLINER")
|
||||
row.prop(self.sprops, "classification_source", text="")
|
||||
@@ -145,7 +151,7 @@ class ReferenceUI:
|
||||
else:
|
||||
self.draw_add_bsdd_ui(context)
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
def draw_add_manual_ui(self, context) -> None:
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "classifications", text="")
|
||||
if self.props.is_adding:
|
||||
@@ -158,7 +164,7 @@ class ReferenceUI:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
def draw_add_bsdd_ui(self, context) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, "keyword", text="")
|
||||
row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER")
|
||||
@@ -185,7 +191,7 @@ class ReferenceUI:
|
||||
op.obj = self.obj
|
||||
op.obj_type = self.obj_type
|
||||
|
||||
def draw_add_file_ui(self, context):
|
||||
def draw_add_file_ui(self, context) -> None:
|
||||
if not self.data.data["active_classification_library"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Active Classification Library", icon="ERROR")
|
||||
@@ -219,13 +225,13 @@ class ReferenceUI:
|
||||
"active_library_reference_index",
|
||||
)
|
||||
|
||||
def draw_editable_ui(self):
|
||||
def draw_editable_ui(self) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_classification_reference", text="Save changes", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
|
||||
bonsai.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
|
||||
|
||||
def draw_reference_ui(self, reference):
|
||||
def draw_reference_ui(self, reference: dict[str, Any]) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
if self.file.schema == "IFC2X3":
|
||||
name = reference["ItemReference"] or "No Identification"
|
||||
|
||||
@@ -81,7 +81,7 @@ class EnableEditingConstraint(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Blender.get_constraint_props()
|
||||
props.constraint_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.constraint), props.constraint_attributes)
|
||||
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.constraint), props.constraint_attributes)
|
||||
props.active_constraint_id = self.constraint
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -59,9 +59,11 @@ classes = (
|
||||
operator.ExpandCostItemRate,
|
||||
operator.ExpandCostItems,
|
||||
operator.ExportCostSchedules,
|
||||
operator.ExportCostSchedulesToPDF,
|
||||
operator.HighlightProductCostItem,
|
||||
operator.ImportCostScheduleCsv,
|
||||
operator.RefreshCostScheduleCsv,
|
||||
operator.RemoveCostScheduleCsvLink,
|
||||
operator.LoadCostItemElementQuantities,
|
||||
operator.LoadCostItemQuantities,
|
||||
operator.ShowAssignedCostRate,
|
||||
@@ -84,7 +86,6 @@ classes = (
|
||||
prop.CostItem,
|
||||
prop.CostItemQuantity,
|
||||
prop.CostItemType,
|
||||
prop.CostSchedulesMapping,
|
||||
prop.ScheduleColumn,
|
||||
prop.BIMCostProperties,
|
||||
ui.BIM_PT_cost_schedules,
|
||||
|
||||
@@ -56,9 +56,29 @@ class CostSchedulesData:
|
||||
"cost_values": cls.cost_values(),
|
||||
"quantity_types": cls.quantity_types(),
|
||||
"currency": cls.currency(),
|
||||
"csv_filepaths": cls.get_csv_filepaths(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_csv_filepaths(cls) -> dict[int, str]:
|
||||
"""Get CSV filepaths for cost schedules from document references."""
|
||||
filepaths = {}
|
||||
cost_docs_document = tool.Cost.get_or_create_cost_documents()
|
||||
|
||||
if cost_docs_document:
|
||||
references = tool.Document.get_document_references(cost_docs_document)
|
||||
for reference in references:
|
||||
if reference.Description and "Cost Schedule ID:" in reference.Description:
|
||||
schedule_id_str = reference.Description.split("Cost Schedule ID:")[1].strip()
|
||||
try:
|
||||
schedule_id = int(schedule_id_str)
|
||||
filepaths[schedule_id] = reference.Location
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return filepaths
|
||||
|
||||
@classmethod
|
||||
def currency(cls) -> Union[Currency, None]:
|
||||
unit = tool.Unit.get_project_currency_unit()
|
||||
@@ -171,6 +191,7 @@ class CostSchedulesData:
|
||||
# parametric_quantities.extend(quantities)
|
||||
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
|
||||
data["UnitSymbol"] = "-"
|
||||
data["QuantityType"] = None
|
||||
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
|
||||
if quantities:
|
||||
quantity = quantities[0]
|
||||
|
||||
@@ -25,6 +25,8 @@ import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.cost as core
|
||||
from pathlib import Path
|
||||
|
||||
from typing import get_args, TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
@@ -545,11 +547,16 @@ class SelectCostScheduleProducts(bpy.types.Operator):
|
||||
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
bl_idname = "bim.import_cost_schedule_csv"
|
||||
bl_label = "Import Cost Schedule CSV"
|
||||
bl_description = "Import cost schdule from the provided .csv file."
|
||||
bl_description = "Import cost schedule from the provided .csv file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
is_schedule_of_rates: bpy.props.BoolProperty(name="Is Schedule Of Rates", default=False)
|
||||
use_relative_path: bpy.props.BoolProperty(
|
||||
name="Use Relative Path",
|
||||
description="Store the CSV filepath relative to the currently opened IFC file",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -560,22 +567,82 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
|
||||
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
|
||||
return {"FINISHED"}
|
||||
|
||||
store_path = self.filepath
|
||||
if self.use_relative_path:
|
||||
store_path = tool.Ifc.get_uri(self.filepath, use_relative_path=True)
|
||||
|
||||
resolved_path = Path(tool.Ifc.resolve_uri(self.filepath))
|
||||
if not resolved_path.exists():
|
||||
self.report({"ERROR"}, f"File does not exist: '{store_path}' (resolved to '{resolved_path}')")
|
||||
return {"CANCELLED"}
|
||||
|
||||
cost_schedule = core.import_cost_schedule_csv(tool.Cost, str(resolved_path), self.is_schedule_of_rates)
|
||||
if cost_schedule:
|
||||
core.add_csv_filepath(tool.Cost, store_path, self.is_schedule_of_rates, cost_schedule)
|
||||
return {"FINISHED"}
|
||||
return {"CANCELLED"}
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row()
|
||||
row.prop(self, "is_schedule_of_rates")
|
||||
row = self.layout.row()
|
||||
row.prop(self, "use_relative_path")
|
||||
|
||||
|
||||
class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.refresh_cost_schedule_csv"
|
||||
bl_label = "Refresh Cost Schedule CSV"
|
||||
bl_description = "Refresh cost schedule data from the associated CSV file"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.active_cost_schedule_id:
|
||||
cls.poll_message_set("No active cost schedule")
|
||||
return False
|
||||
|
||||
file_path = tool.Cost.get_cost_schedule_csv_filepath(props.active_cost_schedule_id)
|
||||
if not file_path:
|
||||
cls.poll_message_set("No CSV file associated with this cost schedule")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost)
|
||||
core.refresh_cost_schedule_csv(tool.Cost)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveCostScheduleCsvLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_cost_schedule_csv_link"
|
||||
bl_label = "Remove CSV Link"
|
||||
bl_description = "Remove the link to the CSV file and delete the related document reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.active_cost_schedule_id:
|
||||
cls.poll_message_set("No active cost schedule")
|
||||
return False
|
||||
|
||||
file_path = tool.Cost.get_cost_schedule_csv_filepath(props.active_cost_schedule_id)
|
||||
if not file_path:
|
||||
cls.poll_message_set("No CSV file associated with this cost schedule")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_cost_schedule_csv_link(tool.Cost)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
|
||||
class AddCostColumn(bpy.types.Operator):
|
||||
bl_idname = "bim.add_cost_column"
|
||||
bl_label = "Add Cost Column"
|
||||
@@ -785,6 +852,81 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
self.layout.label(text="Select a directory.")
|
||||
|
||||
|
||||
class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_cost_schedules_to_pdf"
|
||||
bl_label = "Export Cost Schedule to PDF"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Print chosen cost schedule to pdf."
|
||||
filename_ext = ".pdf"
|
||||
filter_glob: bpy.props.StringProperty(default="*.pdf", options={"HIDDEN"}, maxlen=255)
|
||||
|
||||
cost_schedules_items = []
|
||||
|
||||
def get_cost_schedules_enum_items(self, context):
|
||||
return ExportCostSchedulesToPDF.cost_schedules_items
|
||||
|
||||
cost_schedules_enum: bpy.props.EnumProperty(
|
||||
name="",
|
||||
description="Choose IfcCostSchedule to print",
|
||||
items=get_cost_schedules_enum_items,
|
||||
)
|
||||
|
||||
should_print_summary: bpy.props.BoolProperty(
|
||||
name="Should print summary",
|
||||
description="Print summary at the end of the document",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
box.label(text="Select Ifc Cost Schedule:")
|
||||
box.prop(self, "cost_schedules_enum", text="")
|
||||
layout.separator()
|
||||
box = layout.box()
|
||||
box.label(text="Export properties:")
|
||||
box.prop(self, "should_print_summary")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
try:
|
||||
import typst
|
||||
|
||||
return True
|
||||
except:
|
||||
cls.poll_message_set(
|
||||
"Typst not available.\nIt can be installed from Quality and\nControl -> Debug and using 'typst' with Pip Install.\n(Run Blender as Administrator)"
|
||||
)
|
||||
return False
|
||||
|
||||
def invoke(self, context, event):
|
||||
ExportCostSchedulesToPDF.cost_schedules_items.clear()
|
||||
file = tool.Ifc.get()
|
||||
schedules = file.by_type("IfcCostSchedule")
|
||||
for schedule in schedules:
|
||||
ExportCostSchedulesToPDF.cost_schedules_items.append(
|
||||
(
|
||||
str(schedule.id()),
|
||||
"{} ({})".format(
|
||||
schedule.Name if schedule.Name is not None else "Unnamed",
|
||||
schedule.PredefinedType if schedule.PredefinedType is not None else "UNTYPED",
|
||||
),
|
||||
"",
|
||||
)
|
||||
)
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
file = tool.Ifc.get()
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
cost_schedule = file.by_id(int(self.cost_schedules_enum))
|
||||
options = {"should_print_summary": self.should_print_summary}
|
||||
core.export_cost_schedules_to_pdf(
|
||||
tool.Cost, filepath=self.filepath, cost_schedule=cost_schedule, options=options
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.clear_cost_item_assignments"
|
||||
bl_label = "Clear Cost Item Product Assignments"
|
||||
|
||||
@@ -175,15 +175,6 @@ class CostItemQuantity(PropertyGroup):
|
||||
total_cost_quantity: float
|
||||
|
||||
|
||||
class CostSchedulesMapping(PropertyGroup):
|
||||
cost_schedule_id: IntProperty(name="cost_schedule_id")
|
||||
csv_filepath: StringProperty(name="filepath")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_schedule_id: int
|
||||
csv_filepath: str
|
||||
|
||||
|
||||
class CostItemType(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
@@ -289,7 +280,6 @@ class BIMCostProperties(PropertyGroup):
|
||||
custom_currency: StringProperty(
|
||||
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
|
||||
)
|
||||
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostSchedulesMapping)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_schedule_predefined_types: str
|
||||
@@ -344,7 +334,6 @@ class BIMCostProperties(PropertyGroup):
|
||||
show_cost_item_operators: bool
|
||||
currency: str
|
||||
custom_currency: str
|
||||
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostSchedulesMapping]
|
||||
|
||||
@property
|
||||
def active_cost_item(self) -> Union[CostItem, None]:
|
||||
|
||||
@@ -53,6 +53,7 @@ class BIM_PT_cost_schedules(Panel):
|
||||
if CostSchedulesData.data["total_cost_schedules"]:
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.export_cost_schedules", icon="EXPORT", text="Export All Schedules")
|
||||
row.operator("bim.export_cost_schedules_to_pdf", icon="OUTPUT", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
|
||||
else:
|
||||
@@ -76,17 +77,14 @@ class BIM_PT_cost_schedules(Panel):
|
||||
col = row0.column()
|
||||
col.label(text="Linked CSV:")
|
||||
row_1 = col.row(align=True)
|
||||
if self.props.active_cost_schedule_id in [item.cost_schedule_id for item in self.props.cost_schedule_files]:
|
||||
file = next(
|
||||
(
|
||||
item.csv_filepath
|
||||
for item in self.props.cost_schedule_files
|
||||
if item.cost_schedule_id == self.props.active_cost_schedule_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
row_1.label(text=file)
|
||||
|
||||
csv_filepaths = CostSchedulesData.data["csv_filepaths"]
|
||||
file_path = csv_filepaths.get(self.props.active_cost_schedule_id)
|
||||
|
||||
if file_path:
|
||||
row_1.label(text=file_path)
|
||||
row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="")
|
||||
row_1.operator("bim.remove_cost_schedule_csv_link", icon="X", text="")
|
||||
else:
|
||||
row_1.label(text="No CSV file found")
|
||||
row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="")
|
||||
@@ -226,7 +224,7 @@ class BIM_PT_cost_schedules(Panel):
|
||||
quantities = CostSchedulesData.data["cost_quantities"]
|
||||
row = self.layout.row(align=True)
|
||||
# In IFC, all quantities of IfcCostTime should have 1 type.
|
||||
if quantities:
|
||||
if quantities and cost_item.get("QuantityType"):
|
||||
quantity_class = cost_item["QuantityType"]
|
||||
row.label(text=quantity_class)
|
||||
else:
|
||||
|
||||
@@ -239,7 +239,7 @@ class ProfileImportIFC(bpy.types.Operator):
|
||||
import cProfile
|
||||
import pstats
|
||||
|
||||
profile_file = Path(profile_filename)
|
||||
profile_file = Path(self.profile_filename)
|
||||
cProfile.run("import bpy; bpy.ops.bim.load_project_elements()", str(profile_file))
|
||||
p = pstats.Stats(str(profile_file))
|
||||
p.sort_stats("cumulative").print_stats(50)
|
||||
@@ -824,6 +824,10 @@ class PipInstall(bpy.types.Operator):
|
||||
bl_description = "Installs a package from PyPI"
|
||||
name: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
return f"Installs a package from PyPI: '{properties.name}'."
|
||||
|
||||
def execute(self, context):
|
||||
blender_path = Path(bpy.app.binary_path).parent
|
||||
target = next(
|
||||
|
||||
@@ -86,8 +86,8 @@ class SheetsData:
|
||||
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
|
||||
if not titleblocks_dir:
|
||||
props = tool.Drawing.get_document_props()
|
||||
titleblocks_dir = props.titleblocks_dir
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
titleblocks_dir = prefs.doc.titleblocks_dir
|
||||
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
|
||||
if os.path.exists(titleblocks_dir):
|
||||
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
|
||||
|
||||
@@ -426,8 +426,8 @@ class BaseDecorator:
|
||||
# font_size = 16 <-- this is a good default
|
||||
# TODO: need to synchronize it better with svg
|
||||
|
||||
props = tool.Drawing.get_document_props()
|
||||
magic_font_scale = props.magic_font_scale
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
magic_font_scale = prefs.doc.magic_font_scale
|
||||
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
|
||||
pos = pos - line_no * font_size_px * rotation_matrix[1]
|
||||
|
||||
@@ -1661,7 +1661,7 @@ class CutDecorator:
|
||||
all_vertex_i_offset = 0
|
||||
selected_vertex_i_offset = 0
|
||||
|
||||
classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut
|
||||
classes_no_cut_str = self.addon_prefs.doc.classes_no_cut
|
||||
classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")]
|
||||
|
||||
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
|
||||
@@ -1959,8 +1959,8 @@ class DecorationsHandler:
|
||||
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
|
||||
self.decorators[object_type] = self.decorators["FALL"]
|
||||
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
|
||||
props = tool.Drawing.get_document_props()
|
||||
if drawing_font := props.drawing_font:
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if drawing_font := prefs.doc.drawing_font:
|
||||
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
|
||||
if drawing_font_path.is_file():
|
||||
font_id = blf.load(drawing_font_path.__str__())
|
||||
|
||||
@@ -334,7 +334,15 @@ class CreateDrawing(bpy.types.Operator):
|
||||
annotation_svg = None
|
||||
|
||||
with profile("Generate underlay"):
|
||||
underlay_svg = self.generate_underlay(context)
|
||||
if ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
|
||||
drawing_style = self.cprops.get_active_drawing_style()
|
||||
if not drawing_style:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Failed to create drawing '{self.drawing.Name}' - drawing has underlay but there's no active drawing underlay style.",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
underlay_svg = self.generate_underlay(context)
|
||||
|
||||
with profile("Generate linework"):
|
||||
if tool.Drawing.is_camera_orthographic():
|
||||
@@ -874,6 +882,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
cached_linework -= edited_guids
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
files = {bim_props.ifc_file: tool.Ifc.get()}
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
@@ -890,7 +899,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# Don't use draw.main() just whilst we're prototyping and experimenting
|
||||
# TODO: hash paths are never used
|
||||
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
|
||||
ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5")
|
||||
ifc_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
|
||||
|
||||
self.serialiser.setFile(ifc)
|
||||
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
|
||||
@@ -1470,6 +1479,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
|
||||
is_closed_polygon = False
|
||||
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
||||
# Temporary hack.
|
||||
if "d" not in path.attrib:
|
||||
continue
|
||||
for subpath in path.attrib["d"].split("M")[1:]:
|
||||
subpath_co = "M" + subpath.strip(" Z")
|
||||
# Round due to inaccuracies from Blender meshes and bisection
|
||||
@@ -1726,7 +1738,9 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.props = tool.Drawing.get_document_props()
|
||||
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
|
||||
sheet_item = tool.Drawing.get_active_sheet_item()
|
||||
assert sheet_item
|
||||
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.update_sheet_drawing_sizes(sheet)
|
||||
core.open_layout(tool.Drawing, sheet=sheet)
|
||||
@@ -1790,14 +1804,20 @@ class OpenSheet(bpy.types.Operator):
|
||||
tool.Ifc.get().by_id(s.ifc_definition_id) for s in self.props.sheets if s.is_sheet and s.is_selected
|
||||
]
|
||||
else:
|
||||
sheets = [tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)]
|
||||
sheet_item = tool.Drawing.get_active_sheet_item()
|
||||
assert sheet_item
|
||||
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
|
||||
|
||||
sheet_uris = []
|
||||
sheets_not_found = []
|
||||
sheet_uris: list[str] = []
|
||||
sheets_not_found: list[str] = []
|
||||
warnings: list[tool.Drawing.SheetWarningType] = []
|
||||
|
||||
for sheet in sheets:
|
||||
if not sheet.is_a("IfcDocumentInformation"):
|
||||
continue
|
||||
warnings.extend(sheets_warnings := tool.Drawing.validate_sheet_files(sheet))
|
||||
if sheets_warnings:
|
||||
continue
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
references = sheet_builder.build(sheet)
|
||||
sheet_uri = references["SHEET"]
|
||||
@@ -1807,6 +1827,11 @@ class OpenSheet(bpy.types.Operator):
|
||||
if not os.path.exists(sheet_uri):
|
||||
sheets_not_found.append(sheet.Name)
|
||||
|
||||
if warnings:
|
||||
self.report({"ERROR"}, f"There were errors opening sheets. See system console for the details.")
|
||||
print("-" * 10)
|
||||
print("\n".join(str(w) for w in warnings))
|
||||
|
||||
if sheets_not_found:
|
||||
msg = "Some sheets .svg/.pdf files were not found, need to create them first: \n{}.".format(
|
||||
"\n".join(sheets_not_found)
|
||||
@@ -1832,8 +1857,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def poll(cls, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
# Won't be visible in UI anyway.
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
if not props.sheets or not bim_props.data_dir:
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if not props.sheets or not prefs.data_dir:
|
||||
return False
|
||||
if not tool.Drawing.get_active_drawing_item():
|
||||
cls.poll_message_set("No drawing selected.")
|
||||
@@ -1846,7 +1871,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
assert active_drawing
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
active_sheet = tool.Drawing.get_active_sheet(context)
|
||||
active_sheet = tool.Drawing.get_active_sheet()
|
||||
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
|
||||
drawing_reference = tool.Drawing.get_drawing_document(drawing)
|
||||
|
||||
@@ -1938,8 +1963,8 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
|
||||
cls.poll_message_set("No sheet selected.")
|
||||
return False
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
return props.sheets and bim_props.data_dir
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return props.sheets and prefs.data_dir
|
||||
|
||||
def invoke(self, context, event):
|
||||
# opening all sheets on shift+click
|
||||
@@ -1960,9 +1985,18 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.create_all:
|
||||
sheets = [tool.Ifc.get().by_id(s.ifc_definition_id) for s in props.sheets if s.is_sheet and s.is_selected]
|
||||
else:
|
||||
sheets = [tool.Ifc.get().by_id(props.sheets[props.active_sheet_index].ifc_definition_id)]
|
||||
sheet_item = tool.Drawing.get_active_sheet_item()
|
||||
assert sheet_item
|
||||
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
|
||||
|
||||
warnings: list[tool.Drawing.SheetWarningType] = []
|
||||
n_sheets_created = 0
|
||||
for sheet in sheets:
|
||||
|
||||
warnings.extend(sheet_warnings := tool.Drawing.validate_sheet_files(sheet))
|
||||
if sheet_warnings:
|
||||
continue
|
||||
|
||||
# Update any drawing boundary changes
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.update_sheet_drawing_sizes(sheet)
|
||||
@@ -2023,8 +2057,16 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, pdf)
|
||||
else:
|
||||
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, svg)
|
||||
|
||||
n_sheets_created += 1
|
||||
|
||||
if not self.open_viewer:
|
||||
self.report({"INFO"}, f"{len(sheets)} sheets created...")
|
||||
self.report({"INFO"}, f"{n_sheets_created} sheets created...")
|
||||
|
||||
if warnings:
|
||||
self.report({"ERROR"}, f"There were errors creating sheets. See system console for the details.")
|
||||
print("-" * 10)
|
||||
print("\n".join(str(w) for w in warnings))
|
||||
|
||||
|
||||
class SelectAllDrawings(bpy.types.Operator):
|
||||
@@ -2109,7 +2151,10 @@ class ActivateModel(bpy.types.Operator):
|
||||
bl_idname = "bim.activate_model"
|
||||
bl_label = "Activate Model"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Activate the model view, hide all annotations"
|
||||
bl_description = (
|
||||
"Activate the model view.\n\n"
|
||||
"Show all objects (and apply status filters if they were enabled before) and hide all annotations."
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
@@ -2139,7 +2184,7 @@ class ActivateModel(bpy.types.Operator):
|
||||
if not bpy.app.background:
|
||||
with context.temp_override(**tool.Blender.get_viewport_context()):
|
||||
bpy.ops.object.hide_view_clear()
|
||||
bpy.ops.bim.activate_status_filters()
|
||||
bpy.ops.bim.activate_status_filters(only_if_enabled=True)
|
||||
|
||||
for obj in context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -2275,7 +2320,6 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
if not tool.Drawing.get_active_drawing_item():
|
||||
cls.poll_message_set("No drawing selected.")
|
||||
return False
|
||||
@@ -2706,13 +2750,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.schedules:
|
||||
cls.poll_message_set("No schedule selected.")
|
||||
return False
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
return props.schedules and props.sheets and bim_props.data_dir
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return props.schedules and props.sheets and prefs.data_dir
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
active_schedule = props.schedules[props.active_schedule_index]
|
||||
active_sheet = tool.Drawing.get_active_sheet(context)
|
||||
active_sheet = tool.Drawing.get_active_sheet()
|
||||
ifc_file = tool.Ifc.get()
|
||||
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
@@ -2780,7 +2824,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
active_reference = props.references[props.active_reference_index]
|
||||
active_sheet = tool.Drawing.get_active_sheet(context)
|
||||
active_sheet = tool.Drawing.get_active_sheet()
|
||||
ifc_file = tool.Ifc.get()
|
||||
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
@@ -3010,7 +3054,7 @@ class AddTextLiteral(bpy.types.Operator):
|
||||
"Path": "RIGHT",
|
||||
"BoxAlignment": "bottom_left",
|
||||
}
|
||||
# emulates `bonsai.bim.helper.import_attributes2(ifc_literal, literal_props.attributes)`
|
||||
# emulates `bonsai.bim.helper.import_attributes(ifc_literal, literal_props.attributes)`
|
||||
for attr_name in literal_attr_values:
|
||||
attr = literal_attributes.add()
|
||||
attr.name = attr_name
|
||||
@@ -3163,7 +3207,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.load_sheets(tool.Drawing)
|
||||
|
||||
props = tool.Drawing.get_document_props()
|
||||
sheets_not_found = []
|
||||
warnings: list[tool.Drawing.SheetWarningType] = []
|
||||
for sheet_prop in props.sheets:
|
||||
if not sheet_prop.is_sheet:
|
||||
continue
|
||||
@@ -3174,12 +3218,14 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
filepath = Path(document_uri)
|
||||
if not filepath.is_file():
|
||||
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
|
||||
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
|
||||
core.regenerate_sheet(tool.Drawing, sheet)
|
||||
res = core.regenerate_sheet(tool.Drawing, sheet)
|
||||
if res:
|
||||
warnings.extend(res)
|
||||
|
||||
if sheets_not_found:
|
||||
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
|
||||
if warnings:
|
||||
self.report({"WARNING"}, f"There were warnings loading sheets. See system console for the details.")
|
||||
print("-" * 10)
|
||||
print("\n".join(str(w) for w in warnings))
|
||||
|
||||
|
||||
class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -3194,8 +3240,9 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
assert context.window_manager
|
||||
self.props = tool.Drawing.get_document_props()
|
||||
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
|
||||
sheet_item = tool.Drawing.get_active_sheet_item()
|
||||
assert sheet_item
|
||||
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
|
||||
if sheet.is_a("IfcDocumentInformation"):
|
||||
self.document_type = "SHEET"
|
||||
self.name = sheet.Name
|
||||
@@ -3223,15 +3270,17 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
row.prop(self, "identification", text="Identification")
|
||||
|
||||
def _execute(self, context):
|
||||
self.props = tool.Drawing.get_document_props()
|
||||
props = tool.Drawing.get_document_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
|
||||
sheet_item = tool.Drawing.get_active_sheet_item()
|
||||
assert sheet_item
|
||||
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
|
||||
if self.document_type == "SHEET":
|
||||
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
|
||||
elif self.document_type == "EMBEDDED":
|
||||
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
|
||||
elif self.document_type == "TITLEBLOCK":
|
||||
titleblock = self.props.titleblock
|
||||
titleblock = props.titleblock
|
||||
reference = sheet
|
||||
sheet = tool.Drawing.get_reference_document(reference)
|
||||
assert sheet
|
||||
|
||||
@@ -423,39 +423,6 @@ class DocProperties(PropertyGroup):
|
||||
active_sheet_index: IntProperty(name="Active Sheet Index")
|
||||
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
|
||||
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations)
|
||||
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
|
||||
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
|
||||
titleblocks_dir: StringProperty(
|
||||
default=os.path.join("layouts", "titleblocks") + os.path.sep, name="Default Titleblocks Directory"
|
||||
)
|
||||
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
|
||||
stylesheet_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet"
|
||||
)
|
||||
schedules_stylesheet_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "schedule.css"), name="Default Stylesheet for Schedules"
|
||||
)
|
||||
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
|
||||
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
|
||||
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
|
||||
shadingstyles_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
|
||||
)
|
||||
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
|
||||
drawing_font: StringProperty(default="OpenGost Type B TT.ttf", name="Drawing Font")
|
||||
magic_font_scale: bpy.props.FloatProperty(default=0.004118616, name="Font Scale Factor")
|
||||
imperial_precision: StringProperty(default="1/32", name="Imperial Precision")
|
||||
tolerance: bpy.props.FloatProperty(default=0.00001, name="A tolerance used when selecting objects")
|
||||
classes_to_wireframe: StringProperty(
|
||||
default="IfcVirtualElement",
|
||||
name="Classes to Wireframe",
|
||||
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
|
||||
)
|
||||
classes_no_cut: StringProperty(
|
||||
default="IfcVirtualElement, IfcSpace",
|
||||
name="Classes that are not cut",
|
||||
description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
should_use_underlay_cache: bool
|
||||
@@ -480,23 +447,6 @@ class DocProperties(PropertyGroup):
|
||||
active_sheet_index: int
|
||||
drawing_styles: bpy.types.bpy_prop_collection_idprop[DrawingStyle]
|
||||
should_draw_decorations: bool
|
||||
sheets_dir: str
|
||||
layouts_dir: str
|
||||
titleblocks_dir: str
|
||||
drawings_dir: str
|
||||
stylesheet_path: str
|
||||
schedules_stylesheet_path: str
|
||||
markers_path: str
|
||||
symbols_path: str
|
||||
patterns_path: str
|
||||
shadingstyles_path: str
|
||||
shadingstyle_default: str
|
||||
drawing_font: str
|
||||
magic_font_scale: float
|
||||
imperial_precision: str
|
||||
tolerance: float
|
||||
classes_to_wireframe: str
|
||||
classes_no_cut: str
|
||||
|
||||
def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]:
|
||||
drawing_id = self.active_drawing_id
|
||||
|
||||
@@ -74,10 +74,11 @@ class Scheduler:
|
||||
self.schedule_xlsx(infile, outfile)
|
||||
|
||||
def parse_css(self, infile: str) -> None:
|
||||
props = tool.Drawing.get_document_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
stylesheet_path = os.path.splitext(infile)[0] + ".css"
|
||||
if not os.path.exists(stylesheet_path):
|
||||
stylesheet_rel_path = props.schedules_stylesheet_path
|
||||
stylesheet_rel_path = prefs.doc.schedules_stylesheet_path
|
||||
ifc_file_path = os.path.dirname(tool.Ifc.get_path())
|
||||
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
|
||||
if not os.path.exists(stylesheet_path):
|
||||
|
||||
@@ -285,9 +285,7 @@ class SheetBuilder:
|
||||
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib["width"]))
|
||||
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib["height"]))
|
||||
|
||||
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
|
||||
self.references = {"SHEET": None, "RASTER": []}
|
||||
|
||||
def build(self, sheet: ifcopenshell.entity_instance) -> dict[str, str]:
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
assert layout_path
|
||||
self.layout_dir = os.path.dirname(layout_path)
|
||||
@@ -313,9 +311,7 @@ class SheetBuilder:
|
||||
with open(sheet_path, "wb") as output:
|
||||
tree.write(output)
|
||||
|
||||
self.references["SHEET"] = sheet_path
|
||||
|
||||
return self.references
|
||||
return {"SHEET": sheet_path}
|
||||
|
||||
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
|
||||
titleblock = root.findall(f'{SVG}g[@data-type="titleblock"]')[0]
|
||||
|
||||
@@ -102,7 +102,7 @@ class BIM_PT_camera(Panel):
|
||||
box = self.layout.box()
|
||||
box.label(
|
||||
text=f"Resulting image size is {render.resolution_x} x {render.resolution_y} ({round(megapixels, 2)} MP).",
|
||||
icon="WARNING_LARGE",
|
||||
icon="ERROR",
|
||||
)
|
||||
box.label(
|
||||
text=(f"Which is more than {MEGAPIXELS_WARNING_THRESHOLD} megapixels and will require a lot of VRAM.")
|
||||
@@ -419,6 +419,7 @@ class BIM_PT_sheets(Panel):
|
||||
draw_project_not_saved_ui(self)
|
||||
return
|
||||
|
||||
assert self.layout
|
||||
self.props = tool.Drawing.get_document_props()
|
||||
|
||||
if not self.props.is_editing_sheets:
|
||||
@@ -432,8 +433,8 @@ class BIM_PT_sheets(Panel):
|
||||
row.operator("bim.add_sheet", text="", icon="ADD")
|
||||
row.operator("bim.disable_editing_sheets", text="", icon="CANCEL")
|
||||
|
||||
if self.props.sheets and self.props.active_sheet_index < len(self.props.sheets):
|
||||
active_sheet = self.props.sheets[self.props.active_sheet_index]
|
||||
active_sheet = tool.Drawing.get_active_sheet_item()
|
||||
if active_sheet:
|
||||
row = self.layout.row(align=True)
|
||||
row2 = row.row(align=True)
|
||||
if active_sheet.is_sheet:
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pyright: reportAttributeAccessIssue=false
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
|
||||
@@ -36,24 +36,25 @@ class ExecuteIfcFM(bpy.types.Operator, ExportHelper):
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
props = bpy.context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
return f".{props.format}"
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
layout.prop(props, "format")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
if not props.should_load_from_memory and not props.ifc_files.single_file:
|
||||
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
filepaths = []
|
||||
if ifc_file and props.should_load_from_memory:
|
||||
@@ -95,7 +96,7 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper):
|
||||
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
props.spreadsheet_files.clear()
|
||||
dirname = os.path.dirname(self.filepath)
|
||||
for f in self.files:
|
||||
@@ -112,24 +113,24 @@ class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper):
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
props = bpy.context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
return f".{props.format}"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
layout.prop(props, "format")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
if not props.spreadsheet_files:
|
||||
cls.poll_message_set("No spreadsheet files selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
parser = ifcfm.Parser(preset=props.engine)
|
||||
parser.federate([f.name for f in props.spreadsheet_files])
|
||||
writer = ifcfm.Writer(parser)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.fm.data import FMData
|
||||
from bonsai.bim.prop import MultipleFileSelect, StrProperty
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -30,9 +31,10 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
def get_engine(self, context):
|
||||
def get_engine(self: "BIMFMProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not FMData.is_loaded:
|
||||
FMData.load()
|
||||
return FMData.data["engine"]
|
||||
@@ -57,3 +59,10 @@ class BIMFMProperties(PropertyGroup):
|
||||
default="ods",
|
||||
options=set(),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ifc_files: MultipleFileSelect
|
||||
spreadsheet_files: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
should_load_from_memory: bool
|
||||
engine: str
|
||||
format: Literal["csv", "xlsx", "ods"]
|
||||
|
||||
@@ -33,11 +33,11 @@ class BIM_PT_fm(Panel):
|
||||
if not FMData.is_loaded:
|
||||
FMData.load()
|
||||
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
scene = context.scene
|
||||
props = scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
|
||||
if tool.Ifc.get():
|
||||
row = layout.row()
|
||||
|
||||
@@ -193,9 +193,8 @@ def update_is_adding_representation_layer(self: "BIMObjectGeometryProperties", c
|
||||
return
|
||||
return
|
||||
|
||||
if "representation_layer" in self:
|
||||
del self["representation_layer"]
|
||||
del self["is_adding_representation_layer"]
|
||||
self.property_unset("representation_layer")
|
||||
self.property_unset("is_adding_representation_layer")
|
||||
|
||||
|
||||
def update_is_editing_item_layer(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> None:
|
||||
@@ -216,9 +215,8 @@ def update_is_editing_item_layer(self: "BIMObjectGeometryProperties", context: b
|
||||
self.representation_item_layer = str(layer.id())
|
||||
return
|
||||
|
||||
if "representation_item_layer" in self:
|
||||
del self["representation_item_layer"]
|
||||
del self["is_editing_item_layer"]
|
||||
self.property_unset("representation_item_layer")
|
||||
self.property_unset("is_editing_item_layer")
|
||||
|
||||
|
||||
class BIMObjectGeometryProperties(PropertyGroup):
|
||||
|
||||
@@ -525,14 +525,22 @@ class BIM_PT_derived_coordinates(Panel):
|
||||
text += "*"
|
||||
row.operator("bim.edit_object_placement", text=text, icon="EXPORT")
|
||||
|
||||
row = self.layout.row()
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="XYZ Dimensions")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.enabled = False
|
||||
row.prop(context.active_object, "dimensions", text="X", index=0, slider=True)
|
||||
row.prop(context.active_object, "dimensions", text="Y", index=1, slider=True)
|
||||
row.prop(context.active_object, "dimensions", text="Z", index=2, slider=True)
|
||||
area_3d = next((area for area in context.screen.areas if area.type == "VIEW_3D"), None)
|
||||
space_3d = next((space for space in area_3d.spaces if space.type == "VIEW_3D"), None)
|
||||
|
||||
if bpy.context.scene.BIMModelProperties.show_bounding_box:
|
||||
for axis, icon, idx in [("X", "STRIP_COLOR_01", 0), ("Y", "STRIP_COLOR_04", 1), ("Z", "STRIP_COLOR_05", 2)]:
|
||||
row.label(text="", icon=icon)
|
||||
row.prop(context.active_object, "dimensions", text=axis, index=idx)
|
||||
else:
|
||||
row.prop(context.active_object, "dimensions", text="X", index=0)
|
||||
row.prop(context.active_object, "dimensions", text="Y", index=1)
|
||||
row.prop(context.active_object, "dimensions", text="Z", index=2)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Min Global Z")
|
||||
|
||||
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import json
|
||||
from natsort import natsorted
|
||||
|
||||
|
||||
class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -30,6 +31,8 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Load Groups"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
expanded_groups: list[int]
|
||||
|
||||
def _execute(self, context):
|
||||
self.props = tool.Blender.get_group_props()
|
||||
self.expanded_groups = json.loads(self.props.expanded_groups_json)
|
||||
@@ -38,7 +41,7 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
groups = [
|
||||
group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments
|
||||
]
|
||||
sorted_groups = sorted(groups, key=lambda group: group.Name or "Unnamed")
|
||||
sorted_groups = natsorted(groups, key=lambda group: group.Name or "Unnamed")
|
||||
|
||||
for group in sorted_groups:
|
||||
self.load_group(group)
|
||||
@@ -47,21 +50,22 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.disable_editing_group()
|
||||
return {"FINISHED"}
|
||||
|
||||
def load_group(self, group, tree_depth=0):
|
||||
def load_group(self, group: ifcopenshell.entity_instance, tree_depth: int = 0) -> None:
|
||||
new = self.props.groups.add()
|
||||
new.ifc_definition_id = group.id()
|
||||
new.name = group.Name or "Unnamed"
|
||||
new["name"] = group.Name or "Unnamed"
|
||||
new.tree_depth = tree_depth
|
||||
new.has_children = False
|
||||
new.is_expanded = group.id() in self.expanded_groups
|
||||
|
||||
related_groups: list[ifcopenshell.entity_instance]
|
||||
related_groups = [
|
||||
related_object
|
||||
for rel in group.IsGroupedBy or []
|
||||
for related_object in rel.RelatedObjects
|
||||
if related_object.is_a("IfcGroup")
|
||||
]
|
||||
sorted_related_groups = sorted(related_groups, key=lambda group: group.Name or "Unnamed")
|
||||
sorted_related_groups = natsorted(related_groups, key=lambda group: group.Name or "Unnamed")
|
||||
|
||||
for related_group in sorted_related_groups:
|
||||
new.has_children = True
|
||||
@@ -80,6 +84,7 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Blender.get_group_props()
|
||||
expanded_groups: set[int]
|
||||
expanded_groups = set(json.loads(props.expanded_groups_json))
|
||||
if self.option == "Expand":
|
||||
expanded_groups.add(self.ifc_definition_id)
|
||||
@@ -108,6 +113,12 @@ class AddGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
group: bpy.props.IntProperty()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties) -> str:
|
||||
if properties.group:
|
||||
return "Add new subgroup to the active IfcGroup."
|
||||
return "Add new IfcGroup."
|
||||
|
||||
def _execute(self, context):
|
||||
result = ifcopenshell.api.group.add_group(tool.Ifc.get())
|
||||
if self.group:
|
||||
@@ -154,7 +165,7 @@ class EnableEditingGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
props = tool.Blender.get_group_props()
|
||||
props.group_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.group), props.group_attributes)
|
||||
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.group), props.group_attributes)
|
||||
props.active_group_id = self.group
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -38,8 +38,18 @@ def update_active_group_index(self, context):
|
||||
refresh_pset()
|
||||
|
||||
|
||||
def update_name(self: "Group", context: object) -> None:
|
||||
group = tool.Ifc.get_entity_by_id(self.ifc_definition_id)
|
||||
# Theoretically group can be removed outside Group UI.
|
||||
if not group:
|
||||
return
|
||||
if group.Name == self.name:
|
||||
return
|
||||
group.Name = self.name
|
||||
|
||||
|
||||
class Group(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
name: StringProperty(name="Name", update=update_name)
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=False)
|
||||
has_children: BoolProperty(name="Has Children", default=False)
|
||||
@@ -60,6 +70,7 @@ class BIMGroupProperties(PropertyGroup):
|
||||
active_group_index: IntProperty(name="Active Group Index", update=update_active_group_index)
|
||||
active_group_id: IntProperty(name="Active Group Id")
|
||||
expanded_groups_json: StringProperty(name="JSON String", default="[]")
|
||||
"""JSON serialized list[group_id]."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
group_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
|
||||
@@ -129,7 +129,7 @@ class BIM_UL_groups(UIList):
|
||||
active_data,
|
||||
active_propname,
|
||||
index: int,
|
||||
):
|
||||
) -> None:
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
for i in range(0, item.tree_depth):
|
||||
@@ -141,4 +141,4 @@ class BIM_UL_groups(UIList):
|
||||
op.ifc_definition_id = item.ifc_definition_id
|
||||
op.index = index
|
||||
op.option = "Collapse" if item.is_expanded else "Expand"
|
||||
row.label(text=item.name)
|
||||
row.prop(item, "name", text="", emboss=False)
|
||||
|
||||
@@ -82,7 +82,7 @@ class EnableEditingLayer(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Layer.get_layer_props()
|
||||
props.layer_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
|
||||
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
|
||||
props.active_layer_id = self.layer
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from pathlib import Path
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
import traceback
|
||||
import pyradiance
|
||||
|
||||
|
||||
@@ -58,12 +59,14 @@ classes = (
|
||||
def register():
|
||||
bpy.types.Scene.BIMRadianceExporeterProperies = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
|
||||
bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties)
|
||||
pyradiance_path = Path(get_pyradiance_path())
|
||||
bin_path = pyradiance_path / "bin"
|
||||
if bin_path.exists():
|
||||
for file in bin_path.iterdir():
|
||||
if file.is_file():
|
||||
file.chmod(file.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
if pyradiance:
|
||||
pyradiance_path = Path(get_pyradiance_path())
|
||||
bin_path = pyradiance_path / "bin"
|
||||
if bin_path.exists():
|
||||
for file in bin_path.iterdir():
|
||||
if file.is_file():
|
||||
file.chmod(file.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
@@ -18,12 +18,7 @@
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
import pyradiance as pr
|
||||
except ImportError:
|
||||
print("PyRadiance is not available. Rendering functionality will be disabled.")
|
||||
pr = None
|
||||
|
||||
import pyradiance as pr
|
||||
from datetime import datetime
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
@@ -138,10 +133,6 @@ class RadianceRender(bpy.types.Operator):
|
||||
bl_description = "Renders the scene using Radiance"
|
||||
|
||||
def execute(self, context):
|
||||
if pr is None:
|
||||
self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
print("Starting Radiance rendering process...")
|
||||
props = tool.Blender.get_radiance_exporter_props()
|
||||
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
|
||||
|
||||
@@ -293,9 +293,9 @@ class ObjectMaterialData:
|
||||
if item.is_a("IfcMaterialLayer"):
|
||||
total_thickness = item.LayerThickness
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
props = tool.Drawing.get_document_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = props.imperial_precision
|
||||
precision = prefs.doc.imperial_precision
|
||||
else:
|
||||
precision = None
|
||||
formatted_thickness = format_distance(
|
||||
@@ -330,11 +330,12 @@ class ObjectMaterialData:
|
||||
elif cls.material.is_a("IfcMaterialLayerSet"):
|
||||
layers = cls.material.MaterialLayers
|
||||
thickness = sum([l.LayerThickness for l in layers or []])
|
||||
props = tool.Drawing.get_document_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
assert bpy.context.scene
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
precision = None
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = props.imperial_precision
|
||||
precision = prefs.doc.imperial_precision
|
||||
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -28,9 +28,12 @@ import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.material as core
|
||||
import bonsai.bim.module.model.profile as model_profile
|
||||
from typing import Any, Union, TYPE_CHECKING
|
||||
from typing import Any, Union, TYPE_CHECKING, Literal
|
||||
from bonsai.bim.module.model import wall, slab
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import Attribute
|
||||
|
||||
|
||||
class LoadMaterials(bpy.types.Operator):
|
||||
bl_idname = "bim.load_materials"
|
||||
@@ -500,15 +503,17 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
|
||||
props.material_set_attributes.clear()
|
||||
|
||||
if "Usage" in material.is_a():
|
||||
bonsai.bim.helper.import_attributes2(
|
||||
material, props.material_set_usage_attributes, callback=self.import_attributes
|
||||
bonsai.bim.helper.import_attributes(
|
||||
material, props.material_set_usage_attributes, callback=self.import_attributes_callback
|
||||
)
|
||||
bonsai.bim.helper.import_attributes2(material[0], props.material_set_attributes)
|
||||
bonsai.bim.helper.import_attributes(material[0], props.material_set_attributes)
|
||||
else:
|
||||
bonsai.bim.helper.import_attributes2(material, props.material_set_attributes)
|
||||
bonsai.bim.helper.import_attributes(material, props.material_set_attributes)
|
||||
return {"FINISHED"}
|
||||
|
||||
def import_attributes(self, name, prop, data):
|
||||
def import_attributes_callback(
|
||||
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
|
||||
) -> None | Literal[True]:
|
||||
if name == "CardinalPoint":
|
||||
# TODO: complain to buildingSMART
|
||||
cardinal_point_map = {
|
||||
@@ -532,6 +537,7 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
|
||||
18: "right in line with the shear centre",
|
||||
19: "top in line with the shear centre",
|
||||
}
|
||||
assert prop
|
||||
prop.data_type = "enum"
|
||||
prop.enum_items = json.dumps(cardinal_point_map)
|
||||
if data[name]:
|
||||
@@ -646,7 +652,7 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
|
||||
self.props.active_material_set_item_id = self.material_set_item
|
||||
self.props.material_set_item_profile_attributes.clear()
|
||||
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
|
||||
bonsai.bim.helper.import_attributes2(profile, self.props.material_set_item_profile_attributes)
|
||||
bonsai.bim.helper.import_attributes(profile, self.props.material_set_item_profile_attributes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -707,7 +713,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
self.props.material_set_item_material = str(material_set_item.Material.id())
|
||||
|
||||
self.props.material_set_item_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(material_set_item, self.props.material_set_item_attributes)
|
||||
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes)
|
||||
|
||||
if material_set_item.is_a("IfcMaterialProfile"):
|
||||
if material_set_item.Profile and material_set_item.Profile.ProfileName:
|
||||
@@ -882,6 +888,9 @@ class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC project loaded")
|
||||
return False
|
||||
props = tool.Material.get_material_props()
|
||||
active_style = tool.Blender.get_enum_safe(props, "styles")
|
||||
if not active_style:
|
||||
|
||||
@@ -20,6 +20,7 @@ import ifcopenshell
|
||||
import bpy
|
||||
from ifcopenshell.util.doc import get_entity_doc
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.classification.data import MaterialClassificationsData
|
||||
from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData
|
||||
from bonsai.bim.module.profile.data import ProfileData
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
@@ -157,10 +158,16 @@ class Material(PropertyGroup):
|
||||
|
||||
|
||||
class BIMMaterialProperties(PropertyGroup):
|
||||
|
||||
def update_active_material_index(self, context: bpy.types.Context) -> None:
|
||||
if not MaterialClassificationsData.is_loaded:
|
||||
return
|
||||
MaterialClassificationsData.data["references"] = MaterialClassificationsData.references()
|
||||
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
material_type: EnumProperty(items=get_material_types, update=update_material_type, name="Material Type")
|
||||
materials: CollectionProperty(name="Materials", type=Material)
|
||||
active_material_index: IntProperty(name="Active Material Index")
|
||||
active_material_index: IntProperty(name="Active Material Index", update=update_active_material_index)
|
||||
profiles: EnumProperty(items=get_profiles, name="Profiles")
|
||||
active_material_id: IntProperty(name="Active Material ID")
|
||||
material_attributes: CollectionProperty(name="Material Attributes", type=Attribute)
|
||||
@@ -169,9 +176,8 @@ class BIMMaterialProperties(PropertyGroup):
|
||||
contexts: EnumProperty(items=get_contexts, name="Contexts")
|
||||
|
||||
@property
|
||||
def active_material(self):
|
||||
if 0 <= self.active_material_index < len(self.materials):
|
||||
return self.materials[self.active_material_index]
|
||||
def active_material(self) -> Material | None:
|
||||
return tool.Blender.get_active_uilist_element(self.materials, self.active_material_index)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
|
||||
@@ -48,7 +48,7 @@ class BIM_PT_materials(Panel):
|
||||
MaterialsData.load()
|
||||
|
||||
self.props = tool.Material.get_material_props()
|
||||
material = tool.Material.get_active_material_item()
|
||||
material = self.props.active_material
|
||||
material_id = material.ifc_definition_id if material else None
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
@@ -110,7 +110,7 @@ class BIM_PT_materials(Panel):
|
||||
return
|
||||
ifc_definition_id = self.props.active_material_id
|
||||
if self.props.editing_material_type == "ATTRIBUTES":
|
||||
bonsai.bim.helper.draw_attributes(self.props.material_attributes, self.layout)
|
||||
bonsai.bim.helper.draw_attributes(self.props.material_attributes, self.layout, enable_search=True)
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
|
||||
@@ -21,11 +21,11 @@ import bpy
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import gpu_extras
|
||||
import bmesh
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
import math
|
||||
import mathutils
|
||||
from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
@@ -36,6 +36,7 @@ from typing import Union
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
from itertools import chain
|
||||
from typing import Union, Any
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
@@ -1123,3 +1124,274 @@ class FaceAreaDecorator:
|
||||
self.draw_batch("POINTS", data["verts"], decorator_color)
|
||||
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color, alpha=0.5), data["tris"])
|
||||
|
||||
|
||||
class BoundingBoxDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
def __init__(self):
|
||||
context = bpy.context
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
self.decorator_color_x_axis = (*theme.user_interface.axis_x, 1)
|
||||
self.decorator_color_y_axis = (*theme.user_interface.axis_y, 1)
|
||||
self.decorator_color_z_axis = (*theme.user_interface.axis_z, 1)
|
||||
self.decorator_color_wire = (*theme.view_3d.bone_solid, 1)
|
||||
self.decorator_color_special = tool.Blender.get_addon_preferences().decorator_color_special
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(
|
||||
bpy.types.SpaceView3D.draw_handler_add(
|
||||
handler.draw_bounding_box_wire_cube, (context,), "WINDOW", "POST_VIEW"
|
||||
)
|
||||
)
|
||||
cls.handlers.append(
|
||||
bpy.types.SpaceView3D.draw_handler_add(handler.draw_dimension_text, (context,), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except Exception:
|
||||
pass
|
||||
cls.handlers.clear()
|
||||
cls.is_installed = False
|
||||
|
||||
@staticmethod
|
||||
def get_combined_bounding_box_corners(objects):
|
||||
|
||||
if not objects:
|
||||
return None, None, None
|
||||
if len(objects) == 1:
|
||||
bpy.context.scene.transform_orientation_slots[1].type = "LOCAL"
|
||||
obj = objects[0]
|
||||
corners = [obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box]
|
||||
else:
|
||||
bpy.context.scene.transform_orientation_slots[1].type = "GLOBAL"
|
||||
all_corners = []
|
||||
for obj in objects:
|
||||
if hasattr(obj, "bound_box"):
|
||||
all_corners.extend([obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box])
|
||||
min_corner = mathutils.Vector(
|
||||
(min(v.x for v in all_corners), min(v.y for v in all_corners), min(v.z for v in all_corners))
|
||||
)
|
||||
max_corner = mathutils.Vector(
|
||||
(max(v.x for v in all_corners), max(v.y for v in all_corners), max(v.z for v in all_corners))
|
||||
)
|
||||
corners = [
|
||||
mathutils.Vector((min_corner.x, min_corner.y, min_corner.z)),
|
||||
mathutils.Vector((min_corner.x, min_corner.y, max_corner.z)),
|
||||
mathutils.Vector((min_corner.x, max_corner.y, max_corner.z)),
|
||||
mathutils.Vector((min_corner.x, max_corner.y, min_corner.z)),
|
||||
mathutils.Vector((max_corner.x, min_corner.y, min_corner.z)),
|
||||
mathutils.Vector((max_corner.x, min_corner.y, max_corner.z)),
|
||||
mathutils.Vector((max_corner.x, max_corner.y, max_corner.z)),
|
||||
mathutils.Vector((max_corner.x, max_corner.y, min_corner.z)),
|
||||
]
|
||||
edges = [
|
||||
(0, 1, "Z"),
|
||||
(1, 2, "Y"),
|
||||
(2, 3, "Z"),
|
||||
(3, 0, "Y"),
|
||||
(4, 5, "Z"),
|
||||
(5, 6, "Y"),
|
||||
(6, 7, "Z"),
|
||||
(7, 4, "Y"),
|
||||
(0, 4, "X"),
|
||||
(1, 5, "X"),
|
||||
(2, 6, "X"),
|
||||
(3, 7, "X"),
|
||||
]
|
||||
axis_colors = {
|
||||
"X": (0.956, 0.282, 0.322, 1),
|
||||
"Y": (0.565, 0.812, 0.125, 1),
|
||||
"Z": (0.196, 0.529, 0.929, 1),
|
||||
}
|
||||
return corners, edges, axis_colors
|
||||
|
||||
@staticmethod
|
||||
def find_closest_trihedron(corners, edges, region, rv3d):
|
||||
|
||||
min_y = float("inf")
|
||||
best_origin = None
|
||||
for idx, corner in enumerate(corners):
|
||||
screen_co = location_3d_to_region_2d(region, rv3d, corner)
|
||||
if screen_co is not None and screen_co.y < min_y:
|
||||
min_y = screen_co.y
|
||||
best_origin = idx
|
||||
trihedron = [
|
||||
{"X": (0, 4), "Y": (0, 3), "Z": (0, 1)},
|
||||
{"X": (1, 5), "Y": (1, 2), "Z": (1, 0)},
|
||||
{"X": (2, 6), "Y": (2, 1), "Z": (2, 3)},
|
||||
{"X": (3, 7), "Y": (3, 0), "Z": (3, 2)},
|
||||
{"X": (4, 0), "Y": (4, 7), "Z": (4, 5)},
|
||||
{"X": (5, 1), "Y": (5, 6), "Z": (5, 4)},
|
||||
{"X": (6, 2), "Y": (6, 5), "Z": (6, 7)},
|
||||
{"X": (7, 3), "Y": (7, 4), "Z": (7, 6)},
|
||||
]
|
||||
return trihedron[best_origin]
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_text_background(self, context, coords_dim, text_dim):
|
||||
padding = 5
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
color = (*theme.user_interface.wcol_menu_back.inner[:3], 0.5) # unwrap color values and adds alpha
|
||||
top_left = (coords_dim[0] - padding, coords_dim[1] + text_dim[1] + padding)
|
||||
bottom_left = (coords_dim[0] - padding, coords_dim[1] - padding)
|
||||
top_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] + text_dim[1] + padding)
|
||||
bottom_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] - padding)
|
||||
|
||||
verts = [top_left, bottom_left, top_right, bottom_right]
|
||||
gpu.state.blend_set("ALPHA")
|
||||
self.draw_batch("TRIS", verts, color, [(0, 1, 2), (1, 2, 3)])
|
||||
|
||||
def draw_bounding_box_wire_cube(self, context):
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")]
|
||||
if not selected_objects:
|
||||
return
|
||||
corners, edges, axis_colors = self.get_combined_bounding_box_corners(selected_objects)
|
||||
region = bpy.context.region
|
||||
rv3d = bpy.context.region_data
|
||||
gpu.state.line_width_set(2.0)
|
||||
for i1, i2, axis in edges:
|
||||
self.draw_batch("LINES", [corners[i1], corners[i2]], self.decorator_color_wire)
|
||||
closest_indices = self.find_closest_trihedron(corners, edges, region, rv3d)
|
||||
for axis in "XYZ":
|
||||
pair = closest_indices[axis]
|
||||
if pair is not None:
|
||||
i1, i2 = pair
|
||||
color = getattr(self, f"decorator_color_{axis.lower()}_axis")
|
||||
self.draw_batch("LINES", [corners[i1], corners[i2]], color)
|
||||
|
||||
def draw_dimension_text(self, context):
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")]
|
||||
if not selected_objects:
|
||||
return
|
||||
corners, edges, axis_colors = self.get_combined_bounding_box_corners(selected_objects)
|
||||
if not corners:
|
||||
return
|
||||
dims = mathutils.Vector(
|
||||
(
|
||||
(corners[4] - corners[0]).length,
|
||||
(corners[3] - corners[0]).length,
|
||||
(corners[1] - corners[0]).length,
|
||||
)
|
||||
)
|
||||
region = bpy.context.region
|
||||
rv3d = bpy.context.region_data
|
||||
|
||||
addon_prefs = tool.Blender.get_addon_preferences()
|
||||
font_id = 0
|
||||
font_size = tool.Blender.scale_font_size(12)
|
||||
blf.size(font_id, font_size)
|
||||
blf.enable(font_id, blf.SHADOW)
|
||||
blf.shadow(font_id, 6, 0, 0, 0, 1)
|
||||
color = addon_prefs.decorations_colour
|
||||
blf.color(font_id, *color)
|
||||
|
||||
closest_indices = self.find_closest_trihedron(corners, edges, region, rv3d)
|
||||
|
||||
screen_coords = {}
|
||||
for axis_idx, axis in enumerate("XYZ"):
|
||||
pair = closest_indices[axis]
|
||||
if pair is not None:
|
||||
i1, i2 = pair
|
||||
offset_factor = 0.6 - (axis_idx * 0.1)
|
||||
center = corners[i1].lerp(corners[i2], offset_factor)
|
||||
value = getattr(dims, axis.lower())
|
||||
screen_co = location_3d_to_region_2d(region, rv3d, center)
|
||||
if screen_co is not None:
|
||||
screen_coords[axis] = (screen_co, value)
|
||||
|
||||
self.adjust_overlapping_labels(screen_coords)
|
||||
|
||||
for axis, (screen_co, value) in screen_coords.items():
|
||||
blf.position(font_id, screen_co.x, screen_co.y, 0)
|
||||
blf.color(font_id, 1, 1, 1, 1)
|
||||
value_str = f"D{axis.lower()}: " + format_distance(value, hide_units=False)
|
||||
text_length = blf.dimensions(font_id, value_str)
|
||||
self.draw_text_background(context, screen_co, text_length)
|
||||
blf.draw(font_id, value_str)
|
||||
|
||||
blf.disable(font_id, blf.SHADOW)
|
||||
|
||||
def adjust_overlapping_labels(self, screen_coords):
|
||||
font_id = 0
|
||||
text_dimensions = {}
|
||||
for axis, (screen_co, value) in screen_coords.items():
|
||||
value_str = f"D{axis.lower()}: " + format_distance(value, hide_units=False)
|
||||
text_dimensions[axis] = blf.dimensions(font_id, value_str)
|
||||
|
||||
min_spacing = 5
|
||||
|
||||
axes = list(screen_coords.keys())
|
||||
for i in range(len(axes)):
|
||||
for j in range(i + 1, len(axes)):
|
||||
axis1, axis2 = axes[i], axes[j]
|
||||
co1, _ = screen_coords[axis1]
|
||||
co2, _ = screen_coords[axis2]
|
||||
dim1 = text_dimensions[axis1]
|
||||
dim2 = text_dimensions[axis2]
|
||||
|
||||
bounds1 = {
|
||||
"left": co1.x - min_spacing,
|
||||
"right": co1.x + dim1[0] + min_spacing,
|
||||
"top": co1.y + dim1[1] + min_spacing,
|
||||
"bottom": co1.y - min_spacing,
|
||||
}
|
||||
bounds2 = {
|
||||
"left": co2.x - min_spacing,
|
||||
"right": co2.x + dim2[0] + min_spacing,
|
||||
"top": co2.y + dim2[1] + min_spacing,
|
||||
"bottom": co2.y - min_spacing,
|
||||
}
|
||||
|
||||
if (
|
||||
bounds1["left"] < bounds2["right"]
|
||||
and bounds1["right"] > bounds2["left"]
|
||||
and bounds1["bottom"] < bounds2["top"]
|
||||
and bounds1["top"] > bounds2["bottom"]
|
||||
):
|
||||
|
||||
x_overlap = min(bounds1["right"], bounds2["right"]) - max(bounds1["left"], bounds2["left"])
|
||||
y_overlap = min(bounds1["top"], bounds2["top"]) - max(bounds1["bottom"], bounds2["bottom"])
|
||||
|
||||
if x_overlap < y_overlap:
|
||||
if co1.x < co2.x:
|
||||
co1.x -= x_overlap / 2 + min_spacing
|
||||
co2.x += x_overlap / 2 + min_spacing
|
||||
else:
|
||||
co1.x += x_overlap / 2 + min_spacing
|
||||
co2.x -= x_overlap / 2 + min_spacing
|
||||
else:
|
||||
if co1.y < co2.y:
|
||||
co1.y -= y_overlap / 2 + min_spacing
|
||||
co2.y += y_overlap / 2 + min_spacing
|
||||
else:
|
||||
co1.y += y_overlap / 2 + min_spacing
|
||||
co2.y -= y_overlap / 2 + min_spacing
|
||||
|
||||
@@ -371,7 +371,9 @@ def get_vertical_profile_preview_data(
|
||||
return data
|
||||
|
||||
|
||||
def get_horizontal_profile_preview_data(context, relating_type):
|
||||
def get_horizontal_profile_preview_data(
|
||||
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
|
||||
) -> dict[str, Any]:
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile_curve = material.MaterialProfiles[0].Profile
|
||||
@@ -385,7 +387,7 @@ def get_horizontal_profile_preview_data(context, relating_type):
|
||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
return
|
||||
return {}
|
||||
for point in polyline_points:
|
||||
polyline_verts.append(Vector((point.x, point.y, point.z)))
|
||||
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
|
||||
@@ -397,7 +399,7 @@ def get_horizontal_profile_preview_data(context, relating_type):
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
@@ -433,7 +435,7 @@ def get_horizontal_profile_preview_data(context, relating_type):
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
data["verts"] = []
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
|
||||
@@ -38,6 +38,7 @@ import bonsai.core.aggregate
|
||||
import bonsai.core.type
|
||||
import bonsai.core.root
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.model as core
|
||||
import bonsai.core.spatial
|
||||
from . import wall, slab, profile, mep
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
@@ -564,6 +565,7 @@ class SetActiveType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.relating_type_id = str(self.relating_type)
|
||||
|
||||
|
||||
# TODO: not exposed to UI.
|
||||
class AlignProduct(bpy.types.Operator):
|
||||
bl_idname = "bim.align_product"
|
||||
bl_label = "Align Product"
|
||||
|
||||
@@ -25,7 +25,7 @@ from bonsai.bim.prop import ObjProperty
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bpy.types import PropertyGroup, NodeTree
|
||||
from math import pi, radians
|
||||
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
|
||||
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator, BoundingBoxDecorator
|
||||
from bonsai.bim.module.model.door import update_door_modifier_bmesh
|
||||
from bonsai.bim.module.model.window import update_window_modifier_bmesh
|
||||
from bonsai.bim.module.drawing.decoration import CutDecorator
|
||||
@@ -114,6 +114,39 @@ def update_slab_direction_decorator(self: "BIMModelProperties", context: bpy.typ
|
||||
SlabDirectionDecorator.uninstall()
|
||||
|
||||
|
||||
def update_measure_xyz(self: "BIMModelProperties", context: bpy.types.Context) -> None:
|
||||
area_3d = next((area for area in context.screen.areas if area.type == "VIEW_3D"), None)
|
||||
space_3d = next((space for space in area_3d.spaces if space.type == "VIEW_3D"), None) if area_3d else None
|
||||
scene = context.scene
|
||||
|
||||
if self.show_bounding_box:
|
||||
BoundingBoxDecorator.install(context)
|
||||
# Save previous state if not already saved
|
||||
if not self.prev_transform_orientation_slot_type:
|
||||
self.prev_transform_orientation_slot_type = scene.transform_orientation_slots[1].type
|
||||
if not self.prev_show_gizmo_object_translate and space_3d:
|
||||
self.prev_show_gizmo_object_translate = space_3d.show_gizmo_object_translate
|
||||
# Set new state
|
||||
if space_3d:
|
||||
if len(context.selected_objects) == 1:
|
||||
scene.transform_orientation_slots[1].type = "LOCAL"
|
||||
else:
|
||||
scene.transform_orientation_slots[1].type = "GLOBAL"
|
||||
space_3d.show_gizmo_object_translate = True
|
||||
else:
|
||||
BoundingBoxDecorator.uninstall()
|
||||
# Restore previous state
|
||||
if space_3d:
|
||||
prev_orientation = self.prev_transform_orientation_slot_type
|
||||
if prev_orientation:
|
||||
scene.transform_orientation_slots[1].type = prev_orientation
|
||||
self.prev_transform_orientation_slot_type = ""
|
||||
prev_gizmo = self.prev_show_gizmo_object_translate
|
||||
if prev_gizmo is not None:
|
||||
space_3d.show_gizmo_object_translate = prev_gizmo
|
||||
self.prev_show_gizmo_object_translate = False
|
||||
|
||||
|
||||
def update_cut_decorator(self: "BIMModelProperties", context: bpy.types.Context) -> None:
|
||||
if self.show_cut_decorator:
|
||||
CutDecorator.install(bpy.context)
|
||||
@@ -158,14 +191,6 @@ class BIMModelProperties(PropertyGroup):
|
||||
menu_relating_type_id: bpy.props.IntProperty()
|
||||
icon_id: bpy.props.IntProperty()
|
||||
updating: bpy.props.BoolProperty(default=False)
|
||||
occurrence_name_style: bpy.props.EnumProperty(
|
||||
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
|
||||
name="Occurrence Name Style",
|
||||
)
|
||||
occurrence_name_function: bpy.props.StringProperty(
|
||||
name="Occurrence Name Function",
|
||||
description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style",
|
||||
)
|
||||
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id}
|
||||
extrusion_depth: bpy.props.FloatProperty(name="Extrusion Depth", min=0.001, default=42.0, subtype="DISTANCE")
|
||||
cardinal_point: bpy.props.EnumProperty(
|
||||
@@ -248,6 +273,11 @@ class BIMModelProperties(PropertyGroup):
|
||||
default=False,
|
||||
update=update_slab_direction_decorator,
|
||||
)
|
||||
|
||||
prev_transform_orientation_slot_type: bpy.props.StringProperty(name="Previous Gizmo Orientation Type")
|
||||
prev_show_gizmo_object_translate: bpy.props.BoolProperty(name="Previous Gizmo Translate")
|
||||
|
||||
show_bounding_box: bpy.props.BoolProperty(name="Measure XYZ Dimensions", default=False, update=update_measure_xyz)
|
||||
show_cut_decorator: bpy.props.BoolProperty(
|
||||
name="Show Cut Decorator",
|
||||
default=True,
|
||||
@@ -267,8 +297,6 @@ class BIMModelProperties(PropertyGroup):
|
||||
menu_relating_type_id: int
|
||||
icon_id: int
|
||||
updating: bool
|
||||
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
|
||||
occurrence_name_function: str
|
||||
extrusion_depth: float
|
||||
cardinal_point: Literal[
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"
|
||||
|
||||
@@ -60,30 +60,15 @@ class RolesAddressesData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def get_address_list_attributes(cls, address: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
props = tool.Owner.get_owner_props()
|
||||
def get_address_list_attributes(cls, address: ifcopenshell.entity_instance) -> list[str]:
|
||||
if address.is_a("IfcPostalAddress"):
|
||||
names = ["AddressLines"]
|
||||
elif address.is_a("IfcTelecomAddress"):
|
||||
names = ["TelephoneNumbers", "FacsimileNumbers", "ElectronicMailAddresses", "MessagingIDs"]
|
||||
else:
|
||||
assert False, f"Unexpected entity: {address}"
|
||||
for name in names:
|
||||
if name == "AddressLines":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.address_lines)]
|
||||
elif name == "TelephoneNumbers":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.telephone_numbers)]
|
||||
elif name == "FacsimileNumbers":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.facsimile_numbers)]
|
||||
elif name == "ElectronicMailAddresses":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.electronic_mail_addresses)]
|
||||
elif name == "MessagingIDs":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.messaging_ids)]
|
||||
else:
|
||||
assert False, f"Unexpected name: {name}"
|
||||
results.append({"name": name, "items": items})
|
||||
return results
|
||||
|
||||
return names
|
||||
|
||||
|
||||
class PeopleData(RolesAddressesData):
|
||||
@@ -106,7 +91,6 @@ class PeopleData(RolesAddressesData):
|
||||
"name": cls.get_person_name(person),
|
||||
"roles_label": ", ".join([r["label"] for r in roles]),
|
||||
"is_engaged": bool(person.EngagedIn),
|
||||
"list_attributes": cls.get_person_list_attributes(person),
|
||||
"roles": roles,
|
||||
"addresses": cls.get_addresses(person),
|
||||
}
|
||||
@@ -125,23 +109,6 @@ class PeopleData(RolesAddressesData):
|
||||
name += f" ({full_name})"
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def get_person_list_attributes(cls, person: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
props = tool.Owner.get_owner_props()
|
||||
name: tool.Owner.PersonAttributeType
|
||||
for name in ("MiddleNames", "PrefixTitles", "SuffixTitles"):
|
||||
if name == "MiddleNames":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.middle_names)]
|
||||
elif name == "PrefixTitles":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.prefix_titles)]
|
||||
elif name == "SuffixTitles":
|
||||
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.suffix_titles)]
|
||||
else:
|
||||
assert False, name
|
||||
results.append({"name": name, "items": items})
|
||||
return results
|
||||
|
||||
|
||||
class OrganisationsData(RolesAddressesData):
|
||||
data = {}
|
||||
@@ -177,30 +144,28 @@ class OwnerData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"user_person": cls.get_user_person(),
|
||||
"user_organisation": cls.get_user_organisation(),
|
||||
"users": cls.get_users(),
|
||||
"user_person": cls.user_person(),
|
||||
"user_organisation": cls.user_organisation(),
|
||||
"users": cls.users(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_user_person(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
def user_person(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcPerson")]
|
||||
|
||||
@classmethod
|
||||
def get_user_organisation(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
def user_organisation(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcOrganization")]
|
||||
|
||||
@classmethod
|
||||
def get_users(cls) -> list[dict[str, Any]]:
|
||||
props = tool.Owner.get_owner_props()
|
||||
def users(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
for user in tool.Ifc.get().by_type("IfcPersonAndOrganization"):
|
||||
results.append(
|
||||
{
|
||||
"id": user.id(),
|
||||
"label": "{} ({})".format(user.ThePerson[0] or "Unnamed", user.TheOrganization[0] or "Unnamed"),
|
||||
"is_active": props.active_user_id == user.id(),
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -223,7 +188,14 @@ class ActorData:
|
||||
props = tool.Owner.get_owner_props()
|
||||
if not (ifc_class := props.actor_type):
|
||||
ifc_class = cls.actor_type()[0][0]
|
||||
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type(ifc_class)]
|
||||
|
||||
def get_name(entity: ifcopenshell.entity_instance) -> str:
|
||||
if entity.is_a() == "IfcPersonAndOrganization":
|
||||
return f"{get_name(entity.ThePerson)}/{get_name(entity.TheOrganization)}"
|
||||
# 0 IfcPerson/IfcOrganization Id(IFC2X3)/Identification
|
||||
return entity[0] or "Unnamed"
|
||||
|
||||
return [(str(p.id()), get_name(p), "") for p in tool.Ifc.get().by_type(ifc_class)]
|
||||
|
||||
@classmethod
|
||||
def actors(cls) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -54,12 +54,11 @@ def draw_addresses(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
|
||||
props = tool.Owner.get_owner_props()
|
||||
row = box.row(align=True)
|
||||
row.label(text="Addresses")
|
||||
op = row.operator("bim.add_address", icon="LINK_BLEND", text="")
|
||||
|
||||
assert (wm := bpy.context.window_manager)
|
||||
row.prop(wm.operator_properties_last(bpy.ops.bim.add_address.idname()), "ifc_class", text="")
|
||||
op = row.operator("bim.add_address", icon="ADD", text="")
|
||||
op.parent = parent["id"]
|
||||
op.ifc_class = "IfcTelecomAddress"
|
||||
op = row.operator("bim.add_address", icon="APPEND_BLEND", text="")
|
||||
op.parent = parent["id"]
|
||||
op.ifc_class = "IfcPostalAddress"
|
||||
|
||||
for address in parent["addresses"]:
|
||||
if props.active_address_id == address["id"]:
|
||||
@@ -67,18 +66,20 @@ def draw_addresses(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
|
||||
row.operator("bim.edit_address", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_address", icon="CANCEL", text="")
|
||||
bonsai.bim.helper.draw_attributes(props.address_attributes, box)
|
||||
for attribute in address["list_attributes"]:
|
||||
attributes: list[str] = address["list_attributes"]
|
||||
for attribute_name in attributes:
|
||||
row = box.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
row.label(text=attribute_name)
|
||||
op = row.operator("bim.add_address_attribute", icon="ADD", text="")
|
||||
op.name = attribute["name"]
|
||||
op.name = attribute_name
|
||||
|
||||
for item in attribute["items"]:
|
||||
collection = tool.Owner.get_address_collection(attribute_name)
|
||||
for i, item in enumerate(collection):
|
||||
row = box.row(align=True)
|
||||
row.prop(item["prop"], "name", text="")
|
||||
row.prop(item, "name", text="")
|
||||
op = row.operator("bim.remove_address_attribute", icon="REMOVE", text="")
|
||||
op.name = attribute["name"]
|
||||
op.id = item["id"]
|
||||
op.name = attribute_name
|
||||
op.id = i
|
||||
else:
|
||||
row = box.row(align=True)
|
||||
row.label(text=address["label"])
|
||||
@@ -124,18 +125,19 @@ class BIM_PT_people(bpy.types.Panel):
|
||||
row.operator("bim.disable_editing_person", icon="CANCEL", text="")
|
||||
bonsai.bim.helper.draw_attributes(props.person_attributes, box)
|
||||
|
||||
for attribute in person["list_attributes"]:
|
||||
for attribute_name in tool.Owner.PERSON_ATTRIBUTE_TYPES:
|
||||
row = box.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
row.label(text=attribute_name)
|
||||
op = row.operator("bim.add_person_attribute", icon="ADD", text="")
|
||||
op.name = attribute["name"]
|
||||
op.name = attribute_name
|
||||
|
||||
for item in attribute["items"]:
|
||||
collection = tool.Owner.get_names_collection(attribute_name)
|
||||
for i, item in enumerate(collection):
|
||||
row = box.row(align=True)
|
||||
row.prop(item["prop"], "name", text="")
|
||||
row.prop(item, "name", text="")
|
||||
op = row.operator("bim.remove_person_attribute", icon="REMOVE", text="")
|
||||
op.name = attribute["name"]
|
||||
op.id = item["id"]
|
||||
op.name = attribute_name
|
||||
op.id = i
|
||||
|
||||
draw_roles(box, person)
|
||||
draw_addresses(box, person)
|
||||
@@ -241,7 +243,7 @@ class BIM_PT_owner(bpy.types.Panel):
|
||||
|
||||
for user in OwnerData.data["users"]:
|
||||
row = self.layout.row(align=True)
|
||||
if user["is_active"]:
|
||||
if props.active_user_id == user["id"]:
|
||||
row.label(text=user["label"], icon="USER")
|
||||
row.operator("bim.clear_user", icon="KEYFRAME", text="").user = user["id"]
|
||||
else:
|
||||
|
||||
@@ -118,7 +118,7 @@ class EnableEditingProfile(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Profile.get_profile_props()
|
||||
props.profile_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes)
|
||||
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.profile), props.profile_attributes)
|
||||
props.active_profile_id = self.profile
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class BIM_PT_profiles(Panel):
|
||||
self.props = tool.Profile.get_profile_props()
|
||||
|
||||
active_profile = None
|
||||
assert self.layout
|
||||
if self.props.is_editing and (active_profile := tool.Profile.get_active_profile_ui()):
|
||||
preview_collection = ProfileData.preview_collection
|
||||
box = self.layout.box()
|
||||
@@ -139,7 +140,7 @@ class BIM_PT_profiles(Panel):
|
||||
self.draw_editable_ui(context)
|
||||
|
||||
def draw_editable_ui(self, context):
|
||||
bonsai.bim.helper.draw_attributes(self.props.profile_attributes, self.layout)
|
||||
bonsai.bim.helper.draw_attributes(self.props.profile_attributes, self.layout, enable_search=True)
|
||||
|
||||
|
||||
class BIM_UL_profiles(UIList):
|
||||
|
||||
@@ -1594,14 +1594,15 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
|
||||
def execute(self, context):
|
||||
project_props = tool.Project.get_project_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
project_props.use_relative_project_path = self.use_relative_path
|
||||
if project_props.should_disable_undo_on_save:
|
||||
if prefs.should_disable_undo_on_save:
|
||||
old_history_size = tool.Ifc.get().history_size
|
||||
old_undo_steps = context.preferences.edit.undo_steps
|
||||
tool.Ifc.get().history_size = 0
|
||||
context.preferences.edit.undo_steps = 0
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if project_props.should_disable_undo_on_save:
|
||||
if prefs.should_disable_undo_on_save:
|
||||
tool.Ifc.get().history_size = old_history_size
|
||||
context.preferences.edit.undo_steps = old_undo_steps
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -96,7 +96,7 @@ def is_editing_project_library_update(self: "BIMProjectProperties", context: bpy
|
||||
self.editing_project_library_id = int(self.selected_project_library)
|
||||
project_library = library_file.by_id(int(self.selected_project_library))
|
||||
self.project_library_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(project_library, self.project_library_attributes)
|
||||
bonsai.bim.helper.import_attributes(project_library, self.project_library_attributes)
|
||||
self.parent_library = str(tool.Project.get_parent_library(project_library).id())
|
||||
ProjectLibraryData.load() # Show edit icon in enum.
|
||||
return
|
||||
@@ -300,7 +300,6 @@ class BIMProjectProperties(PropertyGroup):
|
||||
)
|
||||
should_use_cpu_multiprocessing: BoolProperty(name="CPU Multiprocessing", default=True)
|
||||
should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False)
|
||||
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
|
||||
should_load_geometry: BoolProperty(name="Load Geometry", default=True)
|
||||
should_clean_mesh: BoolProperty(
|
||||
name="Clean Meshes",
|
||||
@@ -362,9 +361,6 @@ class BIMProjectProperties(PropertyGroup):
|
||||
description="Load indexed maps (UV and color maps)",
|
||||
default=True,
|
||||
)
|
||||
should_disable_undo_on_save: BoolProperty(
|
||||
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
|
||||
)
|
||||
links: CollectionProperty(name="Links", type=Link)
|
||||
active_link_index: IntProperty(name="Active Link Index")
|
||||
export_schema: EnumProperty(items=get_export_schema, name="IFC Schema", update=update_export_schema)
|
||||
@@ -458,7 +454,6 @@ class BIMProjectProperties(PropertyGroup):
|
||||
geometry_library: Literal["opencascade", "cgal", "cgal-simple", "hybrid-cgal-simple-opencascade"]
|
||||
should_use_cpu_multiprocessing: bool
|
||||
should_merge_materials_by_colour: bool
|
||||
should_stream: bool
|
||||
should_load_geometry: bool
|
||||
should_clean_mesh: bool
|
||||
should_cache: bool
|
||||
@@ -474,7 +469,6 @@ class BIMProjectProperties(PropertyGroup):
|
||||
element_offset: int
|
||||
element_limit: int
|
||||
load_indexed_maps: bool
|
||||
should_disable_undo_on_save: bool
|
||||
links: bpy.types.bpy_prop_collection_idprop[Link]
|
||||
active_link_index: int
|
||||
export_schema: str
|
||||
|
||||
@@ -33,16 +33,16 @@ classes = (
|
||||
operator.UnsharePset,
|
||||
operator.BIM_OT_add_property_to_edit,
|
||||
operator.BIM_OT_remove_property_to_edit,
|
||||
operator.BIM_OT_clear_list,
|
||||
operator.BIM_OT_rename_parameters,
|
||||
operator.BIM_OT_bulk_edit_clear_list,
|
||||
operator.BIM_OT_pset_bulk_rename_parameters,
|
||||
operator.BIM_OT_add_edit_custom_property,
|
||||
operator.BIM_OT_bulk_remove_psets,
|
||||
prop.IfcPropertyEnumeratedValue,
|
||||
prop.IfcProperty,
|
||||
prop.PsetProperties,
|
||||
prop.RenameProperties,
|
||||
prop.AddEditProperties,
|
||||
prop.DeletePsets,
|
||||
prop.RenamePropertyEntry,
|
||||
prop.AddEditPropertyEntry,
|
||||
prop.DeletePsetEntry,
|
||||
prop.GlobalPsetProperties,
|
||||
ui.BIM_PT_object_psets,
|
||||
ui.BIM_PT_object_qtos,
|
||||
@@ -71,9 +71,6 @@ def register():
|
||||
bpy.types.Scene.GroupPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
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)
|
||||
|
||||
|
||||
@@ -86,7 +83,4 @@ def unregister():
|
||||
del bpy.types.Scene.GroupPsetProperties
|
||||
del bpy.types.Scene.ProfilePsetProperties
|
||||
del bpy.types.Scene.WorkSchedulePsetProperties
|
||||
del bpy.types.Scene.RenameProperties
|
||||
del bpy.types.Scene.AddEditProperties
|
||||
del bpy.types.Scene.DeletePsets
|
||||
del bpy.types.Scene.GlobalPsetProperties
|
||||
|
||||
@@ -26,7 +26,7 @@ import bonsai.bim.schema
|
||||
|
||||
|
||||
# TODO: Should this cache belong here? Dunno. Maybe.
|
||||
is_expanded = {}
|
||||
is_expanded: dict[int, bool] = {}
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -176,8 +176,8 @@ class MaterialPsetsData(Data):
|
||||
def load(cls):
|
||||
ifc_definition_id = None
|
||||
props = tool.Material.get_material_props()
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id
|
||||
if material := props.active_material:
|
||||
ifc_definition_id = material.ifc_definition_id
|
||||
|
||||
cls.data = {
|
||||
"ifc_definition_id": ifc_definition_id,
|
||||
@@ -189,8 +189,7 @@ class MaterialPsetsData(Data):
|
||||
@classmethod
|
||||
def pset_name(cls):
|
||||
props = tool.Material.get_material_props()
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
material = props.materials[props.active_material_index]
|
||||
if material := props.active_material:
|
||||
if material.ifc_definition_id:
|
||||
material = tool.Ifc.get().by_id(material.ifc_definition_id)
|
||||
category = getattr(material, "Category", None) or None
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import json
|
||||
from bpy.types import Context, OperatorProperties
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.attribute
|
||||
@@ -29,6 +30,11 @@ import bonsai.tool as tool
|
||||
import bonsai.core.pset as core
|
||||
import bonsai.bim.module.pset.data
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.pset.prop import RenamePropertyEntry, AddEditPropertyEntry
|
||||
|
||||
|
||||
class TogglePsetExpansion(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -263,124 +269,179 @@ class CopyPropertyToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
|
||||
class BIM_OT_add_property_to_edit(bpy.types.Operator):
|
||||
bl_label = "Add Edit Rule"
|
||||
bl_label = "Add Property to Edit"
|
||||
bl_idname = "bim.add_property_to_edit"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
option: bpy.props.StringProperty()
|
||||
index: bpy.props.IntProperty(default=-1)
|
||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
||||
)
|
||||
index: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
option: tool.Pset.BulkOperationType
|
||||
index: int
|
||||
|
||||
@classmethod
|
||||
def description(cls, context: bpy.types.Context, properties: bpy.types.OperatorProperties) -> str:
|
||||
return f"Add property entry to for bulk operation '{properties.option}'."
|
||||
|
||||
def execute(self, context):
|
||||
if self.index == -1:
|
||||
getattr(context.scene, self.option).add()
|
||||
tool.Pset.get_bulk_operation_collection(self.option).add()
|
||||
else:
|
||||
getattr(context.scene, self.option)[self.index].enum_values.add()
|
||||
assert self.option == "ADD_EDIT"
|
||||
props = tool.Pset.get_global_pset_props()
|
||||
props.psets_to_add_edit[self.index].enum_values.add()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_remove_property_to_edit(bpy.types.Operator):
|
||||
bl_label = "Remove Property to Be Renamed"
|
||||
bl_label = "Remove Property from Editing"
|
||||
bl_idname = "bim.remove_property_to_edit"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
index2: bpy.props.IntProperty(default=-1)
|
||||
option: bpy.props.StringProperty()
|
||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
index2: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
|
||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
index: int
|
||||
index2: int
|
||||
option: tool.Pset.BulkOperationType
|
||||
|
||||
@classmethod
|
||||
def description(cls, context: bpy.types.Context, properties: bpy.types.OperatorProperties) -> str:
|
||||
return f"Remove property entry from bulk operation '{properties.option}'."
|
||||
|
||||
def execute(self, context):
|
||||
if self.index2 == -1:
|
||||
getattr(context.scene, self.option).remove(self.index)
|
||||
tool.Pset.get_bulk_operation_collection(self.option).remove(self.index)
|
||||
else:
|
||||
getattr(context.scene, self.option)[self.index].enum_values.remove(self.index2)
|
||||
assert self.option == "ADD_EDIT"
|
||||
props = tool.Pset.get_global_pset_props()
|
||||
props.psets_to_add_edit[self.index].enum_values.remove(self.index2)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_clear_list(bpy.types.Operator):
|
||||
class BIM_OT_bulk_edit_clear_list(bpy.types.Operator):
|
||||
bl_label = "Clear List of Properties"
|
||||
bl_idname = "bim.clear_list"
|
||||
bl_idname = "bim.pset_bulk_edit_clear_list"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
option: bpy.props.StringProperty()
|
||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
option: tool.Pset.BulkOperationType
|
||||
|
||||
def execute(self, context):
|
||||
getattr(context.scene, self.option).clear()
|
||||
tool.Pset.get_bulk_operation_collection(self.option).clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class BIM_OT_pset_bulk_rename_parameters(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Rename Parameters"
|
||||
bl_idname = "bim.rename_parameters"
|
||||
bl_idname = "bim.pset_bulk_rename_parameters"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Rename parameters that are subclasses of IfcElement"
|
||||
bl_description = "Rename pset properties for all IfcElements"
|
||||
|
||||
def _execute(self, context):
|
||||
props_to_map = context.scene.RenameProperties
|
||||
props_to_map = tool.Pset.get_global_pset_props().psets_to_rename
|
||||
ifc_file = tool.Ifc.get()
|
||||
all_ifc_elements = ifc_file.by_type("IfcElement")
|
||||
|
||||
properties_map: defaultdict[str, dict[str, str]] = defaultdict(dict)
|
||||
"""pset_name -> {old_name -> new_name}"""
|
||||
for p in props_to_map:
|
||||
properties_map[p.name][p.existing_property_name] = p.new_property_name
|
||||
|
||||
props_renamed = 0
|
||||
for ifc_element in all_ifc_elements:
|
||||
for definition in ifc_element.IsDefinedBy:
|
||||
if definition.is_a("IfcRelDefinesByProperties"):
|
||||
prop_set = definition.RelatingPropertyDefinition
|
||||
self.rename_property(prop_set, props_to_map, ifc_element)
|
||||
props_renamed += self.rename_property(prop_set, properties_map)
|
||||
|
||||
self.report({"INFO"}, "Finished applying changes")
|
||||
self.report({"INFO"}, f"Finished applying changes, {props_renamed} properties renamed.")
|
||||
return {"FINISHED"}
|
||||
|
||||
def rename_property(self, property_set, properties_to_map, ifc_element):
|
||||
def rename_property(
|
||||
self,
|
||||
property_set: ifcopenshell.entity_instance,
|
||||
pset_remap: dict[str, dict[str, str]],
|
||||
) -> int:
|
||||
props_renamed = 0
|
||||
pset_name = property_set.Name
|
||||
if pset_name not in pset_remap:
|
||||
return props_renamed
|
||||
|
||||
properties_remap = pset_remap[pset_name]
|
||||
property_container: tuple[ifcopenshell.entity_instance, ...]
|
||||
if property_set.is_a() == "IfcPropertySet":
|
||||
property_container = property_set.HasProperties
|
||||
elif property_set.is_a() == "IfcElementQuantity":
|
||||
property_container = property_set.Quantities
|
||||
else:
|
||||
assert False
|
||||
|
||||
for obj_prop in property_container:
|
||||
for prop2map in properties_to_map:
|
||||
if prop2map.pset_name != property_set.Name:
|
||||
continue
|
||||
if prop2map.existing_property_name == obj_prop.Name:
|
||||
obj_prop.Name = prop2map.new_property_name
|
||||
prop_name = obj_prop.Name
|
||||
if prop_name not in properties_remap:
|
||||
continue
|
||||
obj_prop.Name = properties_remap[prop_name]
|
||||
props_renamed += 1
|
||||
return props_renamed
|
||||
|
||||
|
||||
class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Add or Edit a Custom Property"
|
||||
bl_idname = "bim.add_edit_custom_property"
|
||||
bl_description = "Edit pset properties for selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
props = context.scene.AddEditProperties
|
||||
entries = tool.Pset.get_global_pset_props().psets_to_add_edit
|
||||
|
||||
elements_changed = 0
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
ifc_element = tool.Ifc.get_entity(obj)
|
||||
if not ifc_element:
|
||||
continue
|
||||
|
||||
for prop in props:
|
||||
for prop in entries:
|
||||
value = getattr(prop, prop.get_value_name())
|
||||
primary_measure_type = prop.primary_measure_type
|
||||
|
||||
if prop.template_type == "IfcPropertyEnumeratedValue":
|
||||
value_ifc_entity = self.generate_enum_entity(prop)
|
||||
elif prop.template_type == "IfcPropertySingleValue":
|
||||
value_ifc_entity = getattr(self.file, f"create{primary_measure_type}")(value)
|
||||
value_ifc_entity = self.file.create_entity(primary_measure_type, value)
|
||||
else:
|
||||
assert False
|
||||
|
||||
new_pset = ifcopenshell.api.pset.add_pset(self.file, product=ifc_element, name=prop.pset_name)
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
self.file, pset=new_pset, properties={prop.property_name: value_ifc_entity}
|
||||
)
|
||||
self.report({"INFO"}, "Finished applying changes")
|
||||
ifcopenshell.api.pset.edit_pset(self.file, pset=new_pset, properties={prop.name: value_ifc_entity})
|
||||
|
||||
if entries:
|
||||
elements_changed += 1
|
||||
self.report({"INFO"}, f"Finished applying changes, {elements_changed} elements changed.")
|
||||
return {"FINISHED"}
|
||||
|
||||
def generate_enum_entity(self, prop):
|
||||
def generate_enum_entity(self, prop: "AddEditPropertyEntry") -> ifcopenshell.entity_instance:
|
||||
prop_type = prop.get_value_name()
|
||||
prop_enum = self.file.create_entity(
|
||||
"IFCPROPERTYENUMERATION",
|
||||
Name=prop.property_name,
|
||||
Name=prop.name,
|
||||
EnumerationValues=tuple(
|
||||
self.file.create_entity(prop.primary_measure_type, ev[prop_type]) for ev in prop.enum_values
|
||||
),
|
||||
)
|
||||
prop_enum_value = self.file.create_entity(
|
||||
"IFCPROPERTYENUMERATEDVALUE",
|
||||
Name=prop.property_name,
|
||||
Name=prop.name,
|
||||
EnumerationValues=tuple(
|
||||
self.file.create_entity(prop.primary_measure_type, ev[prop_type])
|
||||
for ev in prop.enum_values
|
||||
@@ -400,7 +461,10 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
props = context.scene.DeletePsets
|
||||
props = tool.Pset.get_global_pset_props()
|
||||
|
||||
pset_names_to_delete = {p.name for p in props.psets_to_delete}
|
||||
psets_removed = 0
|
||||
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
ifc_element = tool.Ifc.get_entity(obj)
|
||||
@@ -408,19 +472,18 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
psets = ifcopenshell.util.element.get_psets(ifc_element)
|
||||
|
||||
for prop in props:
|
||||
pset = prop.pset_name
|
||||
if pset in psets:
|
||||
try:
|
||||
ifcopenshell.api.pset.remove_pset(
|
||||
self.file,
|
||||
product=ifc_element,
|
||||
pset=self.file.by_id(psets[pset]["id"]),
|
||||
)
|
||||
except KeyError:
|
||||
pass # Sometimes the pset id is not found, I'm not sure why this happens though. - vulevukusej
|
||||
for pset_name, pset_data in psets.items():
|
||||
if pset_name not in pset_names_to_delete:
|
||||
continue
|
||||
|
||||
self.report({"INFO"}, "Finished applying changes")
|
||||
ifcopenshell.api.pset.remove_pset(
|
||||
self.file,
|
||||
product=ifc_element,
|
||||
pset=self.file.by_id(pset_data["id"]),
|
||||
)
|
||||
psets_removed += 1
|
||||
|
||||
self.report({"INFO"}, f"Finished applying changes, {psets_removed} psets removed.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -103,19 +103,19 @@ def get_pset_name(self: "PsetProperties", context: bpy.types.Context) -> tool.Bl
|
||||
return items
|
||||
|
||||
|
||||
def get_object_pset_name(self, context):
|
||||
def get_object_pset_name(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not ObjectPsetsData.is_loaded:
|
||||
ObjectPsetsData.load()
|
||||
return ObjectPsetsData.data["pset_name"]
|
||||
|
||||
|
||||
def get_material_pset_names(self, context):
|
||||
def get_material_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not MaterialPsetsData.is_loaded:
|
||||
MaterialPsetsData.load()
|
||||
return MaterialPsetsData.data["pset_name"]
|
||||
|
||||
|
||||
def get_material_set_pset_names(self, context):
|
||||
def get_material_set_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global psetnames
|
||||
if not ObjectMaterialData.is_loaded:
|
||||
ObjectMaterialData.load()
|
||||
@@ -128,12 +128,14 @@ def get_material_set_pset_names(self, context):
|
||||
return psetnames[ifc_class]
|
||||
|
||||
|
||||
def get_material_set_item_pset_names(self, context) -> list[tuple[str, str, str]]:
|
||||
def get_material_set_item_pset_names(
|
||||
self: "PsetProperties", context: bpy.types.Context
|
||||
) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global psetnames
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
omprops = tool.Material.get_object_material_props(obj)
|
||||
if not omprops.active_material_set_item_id:
|
||||
if not (ifc_definition_id := omprops.active_material_set_item_id):
|
||||
return []
|
||||
ifc_class = tool.Ifc.get().by_id(ifc_definition_id).is_a()
|
||||
if ifc_class not in psetnames:
|
||||
@@ -142,7 +144,7 @@ def get_material_set_item_pset_names(self, context) -> list[tuple[str, str, str]
|
||||
return psetnames[ifc_class]
|
||||
|
||||
|
||||
def get_task_qto_names(self, context):
|
||||
def get_task_qto_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global qtonames
|
||||
ifc_class = "IfcTask"
|
||||
if ifc_class not in qtonames:
|
||||
@@ -151,7 +153,7 @@ def get_task_qto_names(self, context):
|
||||
return qtonames[ifc_class]
|
||||
|
||||
|
||||
def get_resource_pset_names(self, context):
|
||||
def get_resource_pset_names(self: "PsetProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global psetnames
|
||||
rprops = context.scene.BIMResourceProperties
|
||||
rtprops = context.scene.BIMResourceTreeProperties
|
||||
@@ -162,7 +164,7 @@ def get_resource_pset_names(self, context):
|
||||
return psetnames[ifc_class]
|
||||
|
||||
|
||||
def get_resource_qto_names(self, context):
|
||||
def get_resource_qto_names(self: "PsetProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global qtonames
|
||||
rprops = context.scene.BIMResourceProperties
|
||||
rtprops = context.scene.BIMResourceTreeProperties
|
||||
@@ -173,7 +175,7 @@ def get_resource_qto_names(self, context):
|
||||
return qtonames[ifc_class]
|
||||
|
||||
|
||||
def get_group_pset_names(self, context):
|
||||
def get_group_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global psetnames
|
||||
ifc_class = "IfcGroup"
|
||||
if ifc_class not in psetnames:
|
||||
@@ -182,7 +184,7 @@ def get_group_pset_names(self, context):
|
||||
return psetnames[ifc_class]
|
||||
|
||||
|
||||
def get_group_qto_names(self, context):
|
||||
def get_group_qto_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global qtonames
|
||||
ifc_class = "IfcGroup"
|
||||
if ifc_class not in qtonames:
|
||||
@@ -191,7 +193,7 @@ def get_group_qto_names(self, context):
|
||||
return qtonames[ifc_class]
|
||||
|
||||
|
||||
def get_profile_pset_names(self, context):
|
||||
def get_profile_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global psetnames
|
||||
pprops = tool.Profile.get_profile_props()
|
||||
ifc_class = tool.Ifc.get().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a()
|
||||
@@ -201,7 +203,7 @@ def get_profile_pset_names(self, context):
|
||||
return psetnames[ifc_class]
|
||||
|
||||
|
||||
def get_work_schedule_pset_names(self, context):
|
||||
def get_work_schedule_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
global psetnames
|
||||
ifc_class = "IfcWorkSchedule"
|
||||
if ifc_class not in psetnames:
|
||||
@@ -211,7 +213,7 @@ def get_work_schedule_pset_names(self, context):
|
||||
|
||||
|
||||
# TODO: unsafe?
|
||||
def get_qto_name(self, context):
|
||||
def get_qto_name(self: "PsetProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
pset_type = repr(self)
|
||||
prop_type = pset_type.split(".")[-1]
|
||||
if "bpy.data.objects" in pset_type:
|
||||
@@ -223,24 +225,26 @@ def get_qto_name(self, context):
|
||||
results = get_resource_qto_names(self, context)
|
||||
elif prop_type == "GroupPsetProperties":
|
||||
results = get_group_qto_names(self, context)
|
||||
return [("BBIM_CUSTOM", "Custom Qto", "Create a quantity set without using a template."), None] + results
|
||||
else:
|
||||
assert False
|
||||
return [("BBIM_CUSTOM", "Custom Qto", "Create a quantity set without using a template."), None] + list(results)
|
||||
|
||||
|
||||
def get_object_qto_name(self, context):
|
||||
def get_object_qto_name(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not ObjectPsetsData.is_loaded:
|
||||
ObjectPsetsData.load()
|
||||
return ObjectPsetsData.data["qto_name"]
|
||||
|
||||
|
||||
# TODO: unsafe?
|
||||
def get_template_type(self, context):
|
||||
def get_template_type(self: "AddEditPropertyEntry", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
version = tool.Ifc.get_schema()
|
||||
for t in ("IfcPropertySingleValue", "IfcPropertyEnumeratedValue"):
|
||||
yield (t, t, ifcopenshell.util.doc.get_entity_doc(version, t).get("description", ""))
|
||||
|
||||
|
||||
# TODO: unsafe?
|
||||
def get_primary_measure_type(self, context):
|
||||
def get_primary_measure_type(self: "AddEditPropertyEntry", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not AddEditCustomPropertiesData.is_loaded:
|
||||
AddEditCustomPropertiesData.load()
|
||||
return AddEditCustomPropertiesData.data["primary_measure_type"]
|
||||
@@ -249,6 +253,9 @@ def get_primary_measure_type(self, context):
|
||||
class IfcPropertyEnumeratedValue(PropertyGroup):
|
||||
enumerated_values: CollectionProperty(type=Attribute)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
enumerated_values: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
|
||||
|
||||
IfcPropertyValueType = Literal["IfcPropertySingleValue", "IfcPropertyEnumeratedValue"]
|
||||
|
||||
@@ -292,15 +299,20 @@ class PsetProperties(PropertyGroup):
|
||||
prop_value: str
|
||||
|
||||
|
||||
class RenameProperties(PropertyGroup):
|
||||
pset_name: StringProperty(name="Pset")
|
||||
class RenamePropertyEntry(PropertyGroup):
|
||||
name: StringProperty(name="Pset")
|
||||
existing_property_name: StringProperty(name="Existing Property Name")
|
||||
new_property_name: StringProperty(name="New Property Name")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
existing_property_name: str
|
||||
new_property_name: str
|
||||
|
||||
class AddEditProperties(PropertyGroup):
|
||||
|
||||
class AddEditPropertyEntry(PropertyGroup):
|
||||
pset_name: StringProperty(name="Pset")
|
||||
property_name: StringProperty(name="Property")
|
||||
name: StringProperty(name="Property")
|
||||
string_value: StringProperty(name="Value")
|
||||
bool_value: BoolProperty(name="Value")
|
||||
int_value: IntProperty(name="Value")
|
||||
@@ -309,6 +321,17 @@ class AddEditProperties(PropertyGroup):
|
||||
template_type: EnumProperty(items=get_template_type, name="Template Type")
|
||||
enum_values: CollectionProperty(name="Enum Values", type=Attribute)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pset_name: str
|
||||
name: str
|
||||
string_value: str
|
||||
bool_value: bool
|
||||
int_value: int
|
||||
float_value: float
|
||||
primary_measure_type: str
|
||||
template_type: str
|
||||
enum_values: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
|
||||
def get_value_name(self) -> Union[Literal["string_value", "bool_value", "int_value", "float_value"], None]:
|
||||
schema = tool.Ifc.schema()
|
||||
ifc_data_type = schema.declaration_by_name(self.primary_measure_type)
|
||||
@@ -323,10 +346,26 @@ class AddEditProperties(PropertyGroup):
|
||||
return "float_value"
|
||||
|
||||
|
||||
class DeletePsets(PropertyGroup):
|
||||
pset_name: StringProperty(name="Pset")
|
||||
# This class is needed just to make tooltip more descriptive.
|
||||
class DeletePsetEntry(PropertyGroup):
|
||||
name: StringProperty(name="Pset to Remove")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
|
||||
|
||||
class GlobalPsetProperties(PropertyGroup):
|
||||
pset_filter: StringProperty(name="Pset Filter", options={"TEXTEDIT_UPDATE"})
|
||||
qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"})
|
||||
|
||||
# Bulk operations.
|
||||
psets_to_delete: CollectionProperty(type=DeletePsetEntry) # pyright: ignore[reportRedeclaration]
|
||||
psets_to_rename: CollectionProperty(type=RenamePropertyEntry) # pyright: ignore[reportRedeclaration]
|
||||
psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pset_filter: str
|
||||
qto_filter: str
|
||||
psets_to_delete: bpy.types.bpy_prop_collection_idprop[DeletePsetEntry]
|
||||
psets_to_rename: bpy.types.bpy_prop_collection_idprop[RenamePropertyEntry]
|
||||
psets_to_add_edit: bpy.types.bpy_prop_collection_idprop[AddEditPropertyEntry]
|
||||
|
||||
@@ -60,9 +60,9 @@ def draw_single_property(prop: IfcProperty, layout: bpy.types.UILayout, copy_ope
|
||||
value_name,
|
||||
text=prop.metadata.display_name,
|
||||
)
|
||||
if prop.metadata.is_uri:
|
||||
if prop.metadata.special_type == "URI":
|
||||
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
|
||||
op.data_path = prop.metadata.path_from_id("string_value")
|
||||
op.attribute_data_path = tool.Blender.get_full_data_path(prop.metadata)
|
||||
if prop.metadata.is_optional:
|
||||
layout.prop(prop.metadata, "is_null", icon="RADIOBUT_OFF" if prop.metadata.is_null else "RADIOBUT_ON", text="")
|
||||
if copy_operator:
|
||||
@@ -474,16 +474,17 @@ class BIM_PT_material_psets(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
props = tool.Material.get_material_props()
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id
|
||||
if material := props.active_material:
|
||||
ifc_definition_id = material.ifc_definition_id
|
||||
|
||||
if not MaterialPsetsData.is_loaded:
|
||||
MaterialPsetsData.load()
|
||||
elif ifc_definition_id != MaterialPsetsData.data["ifc_definition_id"]:
|
||||
MaterialPsetsData.load()
|
||||
|
||||
props = context.scene.MaterialPsetProperties
|
||||
props = tool.Pset.get_pset_props("", "Material")
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
@@ -523,6 +524,7 @@ class BIM_PT_material_set_item_psets(Panel):
|
||||
if not MaterialSetItemPsetsData.is_loaded:
|
||||
MaterialSetItemPsetsData.load()
|
||||
|
||||
assert self.layout
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
omprops = tool.Material.get_object_material_props(obj)
|
||||
@@ -530,7 +532,7 @@ class BIM_PT_material_set_item_psets(Panel):
|
||||
self.layout.label(text="No Material Set Item Edited.")
|
||||
return
|
||||
|
||||
props = obj.MaterialSetItemPsetProperties
|
||||
props = tool.Pset.get_pset_props(obj.name, "MaterialSetItem")
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
@@ -569,7 +571,8 @@ class BIM_PT_task_qtos(Panel):
|
||||
if not TaskQtosData.is_loaded:
|
||||
TaskQtosData.load()
|
||||
|
||||
props = context.scene.TaskPsetProperties
|
||||
assert self.layout
|
||||
props = tool.Pset.get_pset_props("", "Task")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "qto_name", text="")
|
||||
op = row.operator("bim.add_qto", icon="ADD", text="")
|
||||
@@ -603,7 +606,8 @@ class BIM_PT_resource_qtos(Panel):
|
||||
if not ResourceQtosData.is_loaded:
|
||||
ResourceQtosData.load()
|
||||
|
||||
props = context.scene.ResourcePsetProperties
|
||||
assert self.layout
|
||||
props = tool.Pset.get_pset_props("", "Resource")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "qto_name", text="")
|
||||
op = row.operator("bim.add_qto", icon="ADD", text="")
|
||||
@@ -637,7 +641,8 @@ class BIM_PT_resource_psets(Panel):
|
||||
if not ResourcePsetsData.is_loaded:
|
||||
ResourcePsetsData.load()
|
||||
|
||||
props = context.scene.ResourcePsetProperties
|
||||
assert self.layout
|
||||
props = tool.Pset.get_pset_props("", "Resource")
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
@@ -671,7 +676,8 @@ class BIM_PT_group_qtos(Panel):
|
||||
if not GroupQtosData.is_loaded:
|
||||
GroupQtosData.load()
|
||||
|
||||
props = context.scene.GroupPsetProperties
|
||||
assert self.layout
|
||||
props = tool.Pset.get_pset_props("", "Group")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "qto_name", text="")
|
||||
op = row.operator("bim.add_qto", icon="ADD", text="")
|
||||
@@ -705,7 +711,8 @@ class BIM_PT_group_psets(Panel):
|
||||
if not GroupPsetData.is_loaded:
|
||||
GroupPsetData.load()
|
||||
|
||||
props = context.scene.GroupPsetProperties
|
||||
assert self.layout
|
||||
props = tool.Pset.get_pset_props("", "Group")
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
@@ -749,7 +756,8 @@ class BIM_PT_profile_psets(Panel):
|
||||
):
|
||||
ProfilePsetsData.load()
|
||||
|
||||
props = context.scene.ProfilePsetProperties
|
||||
assert self.layout
|
||||
props = tool.Pset.get_pset_props("", "Profile")
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
@@ -781,7 +789,8 @@ class BIM_PT_work_schedule_psets(Panel):
|
||||
if not WorkSchedulePsetsData.is_loaded:
|
||||
WorkSchedulePsetsData.load()
|
||||
|
||||
props = context.scene.WorkSchedulePsetProperties
|
||||
assert self.layout
|
||||
props = tool.Pset.get_pset_props("", "WorkSchedule")
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
@@ -815,28 +824,28 @@ class BIM_PT_rename_parameters(Panel):
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
props = context.scene.RenameProperties
|
||||
entries = tool.Pset.get_global_pset_props().psets_to_rename
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.add_property_to_edit", icon="ADD")
|
||||
op.option = "RenameProperties"
|
||||
op.option = "RENAME"
|
||||
|
||||
if props:
|
||||
for index, prop in enumerate(props):
|
||||
if entries:
|
||||
for index, prop in enumerate(entries):
|
||||
row = layout.row(align=True)
|
||||
prop_with_search(row, prop, "pset_name", text="")
|
||||
row.prop(prop, "name", text="")
|
||||
row.prop(prop, "existing_property_name", text="")
|
||||
row.prop(prop, "new_property_name", text="")
|
||||
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
|
||||
op.index = index
|
||||
op.option = "RenameProperties"
|
||||
op.option = "RENAME"
|
||||
|
||||
if props:
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.rename_parameters", icon="CHECKMARK")
|
||||
clear = row.operator("bim.clear_list", icon="CANCEL", text="")
|
||||
clear.option = "RenameProperties"
|
||||
row.operator("bim.pset_bulk_rename_parameters", icon="CHECKMARK")
|
||||
clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="")
|
||||
clear.option = "RENAME"
|
||||
|
||||
|
||||
class BIM_PT_add_edit_custom_properties(Panel):
|
||||
@@ -849,30 +858,31 @@ class BIM_PT_add_edit_custom_properties(Panel):
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
props = context.scene.AddEditProperties
|
||||
entries = tool.Pset.get_global_pset_props().psets_to_add_edit
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.add_property_to_edit", icon="ADD")
|
||||
op.option = "AddEditProperties"
|
||||
op.option = "ADD_EDIT"
|
||||
op.index = -1
|
||||
|
||||
if props:
|
||||
for index, prop in enumerate(props):
|
||||
if entries:
|
||||
for index, prop in enumerate(entries):
|
||||
row = layout.row(align=True)
|
||||
prop_with_search(row, prop, "pset_name", text="")
|
||||
row.prop(prop, "property_name", text="")
|
||||
row.prop(prop, "name", text="")
|
||||
if prop.template_type == "IfcPropertySingleValue":
|
||||
row.prop(prop, prop.get_value_name(), text="")
|
||||
prop_with_search(row, prop, "primary_measure_type", text="")
|
||||
row.prop(prop, "template_type", text="")
|
||||
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
|
||||
op.index = index
|
||||
op.option = "AddEditProperties"
|
||||
op.option = "ADD_EDIT"
|
||||
|
||||
if prop.template_type == "IfcPropertyEnumeratedValue":
|
||||
op = row.operator("bim.add_property_to_edit", icon="ADD", text="Add Enum")
|
||||
op.option = "AddEditProperties"
|
||||
op.option = "ADD_EDIT"
|
||||
op.index = index
|
||||
for index2, prop2 in enumerate(prop.enum_values):
|
||||
row = layout.row()
|
||||
@@ -883,13 +893,12 @@ class BIM_PT_add_edit_custom_properties(Panel):
|
||||
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
|
||||
op.index = index
|
||||
op.index2 = index2
|
||||
op.option = "AddEditProperties"
|
||||
op.option = "ADD_EDIT"
|
||||
|
||||
if props:
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.add_edit_custom_property", icon="CHECKMARK", text="Apply Changes")
|
||||
clear = row.operator("bim.clear_list", icon="CANCEL", text="")
|
||||
clear.option = "AddEditProperties"
|
||||
clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="")
|
||||
clear.option = "ADD_EDIT"
|
||||
|
||||
|
||||
class BIM_PT_delete_psets(Panel):
|
||||
@@ -902,23 +911,23 @@ class BIM_PT_delete_psets(Panel):
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
props = context.scene.DeletePsets
|
||||
entries = tool.Pset.get_global_pset_props().psets_to_delete
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.add_property_to_edit", icon="ADD")
|
||||
op.option = "DeletePsets"
|
||||
op.option = "DELETE"
|
||||
|
||||
if props:
|
||||
for index, prop in enumerate(props):
|
||||
if entries:
|
||||
for index, prop in enumerate(entries):
|
||||
row = layout.row(align=True)
|
||||
prop_with_search(row, prop, "pset_name", text="")
|
||||
row.prop(prop, "name", text="")
|
||||
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
|
||||
op.index = index
|
||||
op.option = "DeletePsets"
|
||||
op.option = "DELETE"
|
||||
|
||||
if props:
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.bulk_remove_psets", icon="CHECKMARK", text="Apply Changes")
|
||||
clear = row.operator("bim.clear_list", icon="CANCEL", text="")
|
||||
clear.option = "DeletePsets"
|
||||
clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="")
|
||||
clear.option = "DELETE"
|
||||
|
||||
@@ -779,6 +779,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
row.prop(props, "representation_obj", text="Object")
|
||||
elif props.representation_template == "PROFILESET":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "profile", text="Profile")
|
||||
prop_with_search(self.layout, props, "profile", text="Profile", should_click_ok=True)
|
||||
if props.representation_template != "EMPTY":
|
||||
prop_with_search(self.layout, props, "contexts", should_click_ok=True)
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
from natsort import natsorted
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -39,11 +40,11 @@ class SearchData:
|
||||
cls.data["saved_searches"] = cls.saved_searches()
|
||||
|
||||
@classmethod
|
||||
def saved_searches(cls):
|
||||
def saved_searches(cls) -> list[tuple[str, str, str]]:
|
||||
if not tool.Ifc.get():
|
||||
return []
|
||||
groups = tool.Ifc.get().by_type("IfcGroup")
|
||||
results = []
|
||||
results: list[ifcopenshell.entity_instance] = []
|
||||
for group in groups:
|
||||
try:
|
||||
data = json.loads(group.Description)
|
||||
@@ -51,7 +52,8 @@ class SearchData:
|
||||
results.append(group)
|
||||
except:
|
||||
pass
|
||||
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
|
||||
enum_items = [(str(g.id()), g.Name or "Unnamed", "") for g in results]
|
||||
return natsorted(enum_items, key=lambda x: x[1])
|
||||
|
||||
|
||||
class ColourByPropertyData:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import json
|
||||
import bisect
|
||||
import traceback
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.group
|
||||
@@ -105,6 +106,8 @@ class SelectFilterElements(bpy.types.Operator):
|
||||
query = tool.Search.get_query_for_selected_elements()
|
||||
filter_groups[self.group_index].filters[self.index].value = query
|
||||
if query.startswith("bpy.data.texts['"):
|
||||
text: bpy.types.Text = eval(query)
|
||||
name = text.name
|
||||
self.report({"INFO"}, f'List of Global Ids was saved to the text file "{name}" in the current .blend file')
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -178,10 +181,49 @@ class Search(Operator):
|
||||
class SaveSearch(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.save_search"
|
||||
bl_label = "Save Search"
|
||||
bl_description = "Save search filter to an IFC group"
|
||||
bl_description = (
|
||||
"Save search filter to an IFC group.\n\n"
|
||||
"Search query will be saved to group description, query elements will be assigned to the group."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: StringProperty(name="Name")
|
||||
module: StringProperty()
|
||||
|
||||
name_search_items: list[str] = []
|
||||
|
||||
def get_name_search_items(self, context: object, text: str) -> list[str]:
|
||||
# Extra item so it will be easy to select current text.
|
||||
return [text] + SaveSearch.name_search_items
|
||||
|
||||
name: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Name",
|
||||
search=get_name_search_items,
|
||||
search_options={"SORT"},
|
||||
)
|
||||
module: StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
def update_use_all_ifcgroups(self, context: object = None) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
groups = {
|
||||
g.Name or "Unnamed"
|
||||
for g in ifc_file.by_type("IfcGroup")
|
||||
if self.use_all_ifcgroups or g.ObjectType == "SEARCH"
|
||||
}
|
||||
self.name_search_items[:] = natsorted(groups)
|
||||
|
||||
use_all_ifcgroups: BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Use Any IfcGroup",
|
||||
description=(
|
||||
"By default we're targeting only IfcGroups with SEARCH ObjectType "
|
||||
"to prevent breaking internal IfcGroups (e.g. IfcGroups used for Bonsai drawings).\n\n"
|
||||
"Enabling this option allows saving search to any IfcGroup matched by the provided name.\n"
|
||||
"Use with caution."
|
||||
),
|
||||
update=update_use_all_ifcgroups,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
module: str
|
||||
use_all_ifcgroups: bool
|
||||
|
||||
def _execute(self, context):
|
||||
if not self.name:
|
||||
@@ -193,23 +235,37 @@ class SaveSearch(Operator, tool.Ifc.Operator):
|
||||
query = tool.Search.export_filter_query(filter_groups)
|
||||
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
self.report({"ERROR"}, "Error occurred trying save search.")
|
||||
return
|
||||
|
||||
description = json.dumps({"type": "BBIM_Search", "query": query})
|
||||
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
|
||||
ifc_file = tool.Ifc.get()
|
||||
group = next(
|
||||
(
|
||||
g
|
||||
for g in ifc_file.by_type("IfcGroup")
|
||||
if g.Name == self.name and (self.use_all_ifcgroups or g.ObjectType == "SEARCH")
|
||||
),
|
||||
None,
|
||||
)
|
||||
if group:
|
||||
group = group[0]
|
||||
group.Description = description
|
||||
else:
|
||||
group = ifcopenshell.api.group.add_group(tool.Ifc.get(), name=self.name, description=description)
|
||||
group.ObjectType = "SEARCH"
|
||||
if results:
|
||||
ifcopenshell.api.group.assign_group(tool.Ifc.get(), products=list(results), group=group)
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row()
|
||||
row.prop(self, "name")
|
||||
assert (layout := self.layout)
|
||||
layout.prop(self, "name")
|
||||
layout.prop(self, "use_all_ifcgroups")
|
||||
|
||||
def invoke(self, context, event):
|
||||
assert context.window_manager
|
||||
tool.Search.patch_search_ifcgroups()
|
||||
self.update_use_all_ifcgroups()
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
@@ -227,11 +283,13 @@ class LoadSearch(Operator, tool.Ifc.Operator):
|
||||
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
props = tool.Search.get_search_props()
|
||||
row = self.layout.row()
|
||||
row.prop(props, "saved_searches", text="")
|
||||
|
||||
def invoke(self, context, event):
|
||||
tool.Search.patch_search_ifcgroups()
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
@@ -757,8 +815,8 @@ class SelectSimilar(Operator, tool.Ifc.Operator):
|
||||
def execute(self, context):
|
||||
self.calculated_sum = 0 # reset if run before
|
||||
key = "predefined_type" if self.key == "PredefinedType" else self.key
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
tolerance = dprops.tolerance
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
tolerance = prefs.doc.tolerance
|
||||
formatted_tolerance = f"{tolerance:.{max(0, -int(f'{tolerance:.1e}'.split('e')[-1])) if tolerance < 1 else 1}f}"
|
||||
|
||||
if self.calculate_sum:
|
||||
|
||||
@@ -33,7 +33,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
def get_element_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
@@ -142,18 +142,85 @@ class BIMSearchProperties(PropertyGroup):
|
||||
filter_query: StringProperty(name="Filter Query")
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
facet: EnumProperty(
|
||||
name="Facet",
|
||||
items=[
|
||||
("entity", "Class", "", "FILE_3D", 0),
|
||||
("attribute", "Attribute", "", "COPY_ID", 1),
|
||||
("property", "Property", "", "PROPERTIES", 2),
|
||||
("material", "Material", "", "MATERIAL", 3),
|
||||
("classification", "Classification", "", "OUTLINER", 4),
|
||||
("location", "Location", "", "PACKAGE", 5),
|
||||
("type", "Type", "", "FILE_VOLUME", 6),
|
||||
("group", "Group", "", "OUTLINER_COLLECTION", 7),
|
||||
("parent", "Parent", "", "FILE_PARENT", 8),
|
||||
("query", "Query", "", "POINTCLOUD_DATA", 9),
|
||||
("instance", "GlobalId", "", "GRIP", 10),
|
||||
("entity", "Class", "Search by IFC class.\n\nExample: 'IfcWall'.", "FILE_3D", 0),
|
||||
(
|
||||
"attribute",
|
||||
"Attribute",
|
||||
"Search by IFC class attribute value.\n\nExample values: 'Name', 'Cube'.",
|
||||
"COPY_ID",
|
||||
1,
|
||||
),
|
||||
(
|
||||
"property",
|
||||
"Property",
|
||||
"Search by Pset property value.\n\n"
|
||||
"Example values: 'Pset_WallCommon', 'FireRating', 'equal to', '2HR'.",
|
||||
"PROPERTIES",
|
||||
2,
|
||||
),
|
||||
(
|
||||
"material",
|
||||
"Material",
|
||||
"Search by material name.\n\n"
|
||||
"Example: 'concrete'\n"
|
||||
"(to select all elements with material named 'concrete').",
|
||||
"MATERIAL",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"classification",
|
||||
"Classification",
|
||||
"Search by classification references.\n\n"
|
||||
"Example: 'MyReference'\n"
|
||||
"(to select all elements that have classification reference with Id 'MyReference').",
|
||||
"OUTLINER",
|
||||
4,
|
||||
),
|
||||
(
|
||||
"location",
|
||||
"Location",
|
||||
"Search by spatial element.\n\n"
|
||||
"Example: 'My Storey'\n"
|
||||
"(to select all elements contained in 'My Storey' spatial element).",
|
||||
"PACKAGE",
|
||||
5,
|
||||
),
|
||||
(
|
||||
"type",
|
||||
"Type",
|
||||
"Search by element type.\n\n"
|
||||
"Example: 'BaseType'\n(to select all elements that have element type named 'BaseType').",
|
||||
"FILE_VOLUME",
|
||||
6,
|
||||
),
|
||||
(
|
||||
"group",
|
||||
"Group",
|
||||
"Search by IfcGroup name.\n\n"
|
||||
"Example: 'MyGroup'\n(to select all elements that are assigned to IfcGroup named 'MyGroup').",
|
||||
"OUTLINER_COLLECTION",
|
||||
7,
|
||||
),
|
||||
(
|
||||
"parent",
|
||||
"Parent",
|
||||
"Search by parent element.\n\n"
|
||||
"Example: 'My Building'\n"
|
||||
"(to select all children elements of 'My Building').",
|
||||
"FILE_PARENT",
|
||||
8,
|
||||
),
|
||||
(
|
||||
"query",
|
||||
"Query",
|
||||
"Search elements by special queries.\n\n"
|
||||
"Example values: 'types.count', '0' (to select all elements that have 0 occurrences).",
|
||||
"POINTCLOUD_DATA",
|
||||
9,
|
||||
),
|
||||
("instance", "GlobalId", "Search entity by guid.\n\nExample: '2W83_qKWvEGgeo5v66dTxG'.", "GRIP", 10),
|
||||
],
|
||||
)
|
||||
saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches")
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pyright: reportAttributeAccessIssue=false
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
|
||||
@@ -367,9 +367,10 @@ class WorkPlansData:
|
||||
@classmethod
|
||||
def active_work_plan_schedules(cls):
|
||||
results = []
|
||||
if not bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id:
|
||||
props = tool.Sequence.get_work_plan_props()
|
||||
if not props.active_work_plan_id:
|
||||
return []
|
||||
for rel in tool.Ifc.get().by_id(bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id).IsDecomposedBy:
|
||||
for rel in tool.Ifc.get().by_id(props.active_work_plan_id).IsDecomposedBy:
|
||||
for work_schedule in rel.RelatedObjects:
|
||||
results.append({"id": work_schedule.id(), "name": work_schedule.Name or "Unnamed"})
|
||||
return results
|
||||
|
||||
@@ -94,12 +94,24 @@ class ActivateStatusFilters(bpy.types.Operator):
|
||||
bl_description = "Filter and display objects based on currently selected IFC statuses"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
only_if_enabled: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Only If Filters are Enabled",
|
||||
description="Activate status filters only in case if they were enabled from the UI before.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
only_if_enabled: bool
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Sequence.get_status_props()
|
||||
|
||||
if not props.is_enabled:
|
||||
# In case if operator was added to Quick Favorites.
|
||||
bpy.ops.bim.disable_status_fitlers()
|
||||
if not self.only_if_enabled:
|
||||
# Allow users to use the same operator to refresh filters,
|
||||
# even if they were not enabled before.
|
||||
# Typically would occur when operator is added to Quick Favorites.
|
||||
bpy.ops.bim.enable_status_filters()
|
||||
return {"FINISHED"}
|
||||
|
||||
visible_statuses = {s.name for s in props.statuses if s.is_visible}
|
||||
@@ -140,10 +152,11 @@ class EditWorkPlan(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Edit Work Plan"
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Sequence.get_work_plan_props()
|
||||
core.edit_work_plan(
|
||||
tool.Ifc,
|
||||
tool.Sequence,
|
||||
work_plan=tool.Ifc.get().by_id(context.scene.BIMWorkPlanProperties.active_work_plan_id),
|
||||
work_plan=tool.Ifc.get().by_id(props.active_work_plan_id),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ class BIM_PT_work_plans(Panel):
|
||||
def draw(self, context):
|
||||
if not WorkPlansData.is_loaded:
|
||||
WorkPlansData.load()
|
||||
self.props = context.scene.BIMWorkPlanProperties
|
||||
assert self.layout
|
||||
self.props = tool.Sequence.get_work_plan_props()
|
||||
|
||||
row = self.layout.row()
|
||||
if WorkPlansData.data["total_work_plans"]:
|
||||
@@ -99,7 +100,7 @@ class BIM_PT_work_plans(Panel):
|
||||
for work_plan in WorkPlansData.data["work_plans"]:
|
||||
self.draw_work_plan_ui(work_plan)
|
||||
|
||||
def draw_work_plan_ui(self, work_plan):
|
||||
def draw_work_plan_ui(self, work_plan: dict[str, Any]) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=work_plan["name"], icon="TEXT")
|
||||
if self.props.active_work_plan_id == work_plan["id"]:
|
||||
@@ -121,10 +122,10 @@ class BIM_PT_work_plans(Panel):
|
||||
elif self.props.editing_type == "SCHEDULES":
|
||||
self.draw_work_schedule_ui()
|
||||
|
||||
def draw_editable_ui(self):
|
||||
def draw_editable_ui(self) -> None:
|
||||
draw_attributes(self.props.work_plan_attributes, self.layout)
|
||||
|
||||
def draw_work_schedule_ui(self):
|
||||
def draw_work_schedule_ui(self) -> None:
|
||||
if WorkPlansData.data["has_work_schedules"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "work_schedules", text="")
|
||||
@@ -177,7 +178,8 @@ class BIM_PT_work_schedules(Panel):
|
||||
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
|
||||
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
|
||||
|
||||
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
|
||||
def draw_work_schedule_ui(self, work_schedule_id: int, work_schedule: dict[str, Any]) -> None:
|
||||
assert self.layout
|
||||
if work_schedule["PredefinedType"] == "BASELINE":
|
||||
self.draw_readonly_work_schedule_ui(work_schedule_id)
|
||||
else:
|
||||
@@ -250,7 +252,7 @@ class BIM_PT_work_schedules(Panel):
|
||||
self.draw_column_ui()
|
||||
self.draw_editable_task_ui(work_schedule_id)
|
||||
|
||||
def draw_task_operators(self):
|
||||
def draw_task_operators(self) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
ifc_definition_id = None
|
||||
@@ -285,9 +287,10 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.operator("bim.duplicate_task", text="Copy", icon="DUPLICATE").task = ifc_definition_id
|
||||
row.operator("bim.remove_task", text="Delete", icon="X").task = ifc_definition_id
|
||||
|
||||
def draw_column_ui(self):
|
||||
def draw_column_ui(self) -> None:
|
||||
if not self.props.should_show_column_ui:
|
||||
return
|
||||
assert self.layout
|
||||
row = self.layout.row()
|
||||
row.operator("bim.setup_default_task_columns", text="Setup Default Columns", icon="ANCHOR_BOTTOM")
|
||||
row.alignment = "RIGHT"
|
||||
@@ -318,7 +321,8 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw_editable_work_schedule_ui(self):
|
||||
draw_attributes(self.props.work_schedule_attributes, self.layout)
|
||||
|
||||
def draw_editable_task_ui(self, work_schedule_id):
|
||||
def draw_editable_task_ui(self, work_schedule_id: int) -> None:
|
||||
assert self.layout
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Task Tools")
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -305,10 +305,24 @@ class SelectContainer(bpy.types.Operator):
|
||||
class SelectSimilarContainer(bpy.types.Operator):
|
||||
bl_idname = "bim.select_similar_container"
|
||||
bl_label = "Select Similar Container"
|
||||
bl_description = "Recurvisevly selects all objects in the container.\n\nCtrl+click to select only one level deep"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
is_recursive: bpy.props.BoolProperty(default=True)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.ctrl:
|
||||
self.is_recursive = False
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
core.select_similar_container(tool.Ifc, tool.Spatial, obj=context.active_object)
|
||||
core.select_similar_container(
|
||||
tool.Ifc,
|
||||
tool.Spatial,
|
||||
obj=context.active_object,
|
||||
is_recursive=self.is_recursive,
|
||||
)
|
||||
self.is_recursive = True # <-- forcibly reset
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -431,18 +445,36 @@ class SelectDecomposedElements(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
|
||||
container: bpy.props.IntProperty()
|
||||
is_recursive: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return "Select the active item" + "\nALT+CLICK to select all listed elements"
|
||||
return (
|
||||
"Select the active item"
|
||||
+ "\nALT+CLICK to select all listed elements.\nCTRL + CLICK to select only one level deep"
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
self.should_filter = False
|
||||
if event.type == "LEFTMOUSE":
|
||||
if event.alt:
|
||||
self.should_filter = False
|
||||
if event.ctrl:
|
||||
self.is_recursive = False
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter))
|
||||
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive))
|
||||
|
||||
# Make selected active element in list, the active object
|
||||
props = tool.Spatial.get_spatial_props()
|
||||
active_element = props.active_element
|
||||
if active_element and active_element.type == "OCCURRENCE":
|
||||
ifc_file = tool.Ifc.get()
|
||||
ifc_entity = ifc_file.by_id(active_element.ifc_definition_id)
|
||||
obj = tool.Ifc.get_object(ifc_entity)
|
||||
if obj:
|
||||
context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -23,13 +23,13 @@ import ifcopenshell.api
|
||||
import ifcopenshell.api.aggregate
|
||||
import ifcopenshell.api.group
|
||||
import ifcopenshell.api.structural
|
||||
import ifcopenshell.util.attribute
|
||||
import bonsai.bim.helper
|
||||
import bonsai.core.structural as core
|
||||
import bonsai.tool as tool
|
||||
from math import degrees
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.structural.decorator import LoadsDecorator
|
||||
from typing import Literal, Any, TYPE_CHECKING
|
||||
|
||||
|
||||
class ShowLoads(bpy.types.Operator):
|
||||
@@ -40,6 +40,7 @@ class ShowLoads(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def modal(self, context, event):
|
||||
assert context.screen
|
||||
if event.type == "F5":
|
||||
LoadsDecorator.update()
|
||||
for area in context.screen.areas:
|
||||
@@ -54,6 +55,7 @@ class ShowLoads(bpy.types.Operator):
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
assert context.window and context.window_manager and context.screen
|
||||
collection = bpy.data.collections.get("IfcStructuralItem")
|
||||
if collection is None:
|
||||
self.report({"ERROR"}, "No IfcStructuralItems found.")
|
||||
@@ -87,6 +89,7 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Structural.get_object_structural_props(obj)
|
||||
file = tool.Ifc.get()
|
||||
related_structural_connection = file.by_id(oprops.ifc_definition_id)
|
||||
assert props.relating_structural_member
|
||||
relating_structural_member = tool.Ifc.get_entity(props.relating_structural_member)
|
||||
assert relating_structural_member
|
||||
if not relating_structural_member.is_a("IfcStructuralMember"):
|
||||
@@ -171,39 +174,17 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_structural_boundary_condition"
|
||||
bl_label = "Enable Editing Structural Boundary Condition"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
boundary_condition: bpy.props.IntProperty()
|
||||
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
boundary_condition: int
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Structural.get_object_structural_props(obj)
|
||||
props.boundary_condition_attributes.clear()
|
||||
|
||||
condition = tool.Ifc.get().by_id(self.boundary_condition)
|
||||
schema = tool.Ifc.schema()
|
||||
|
||||
for attribute in schema.declaration_by_name(condition.is_a()).all_attributes():
|
||||
value = getattr(condition, attribute.name(), None)
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
|
||||
new = props.boundary_condition_attributes.add()
|
||||
new.name = attribute.name()
|
||||
new.is_null = value is None
|
||||
new.is_optional = attribute.optional()
|
||||
if isinstance(data_type, tuple) and data_type[0] == "select":
|
||||
enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)]
|
||||
new.enum_items = json.dumps(enum_items)
|
||||
if isinstance(value, bool):
|
||||
new.bool_value = False if new.is_null else value
|
||||
new.data_type = "bool"
|
||||
new.enum_value = "IfcBoolean"
|
||||
elif isinstance(value, float):
|
||||
new.float_value = 0.0 if new.is_null else value
|
||||
new.data_type = "float"
|
||||
new.enum_value = next(i for i in enum_items if i != "IfcBoolean")
|
||||
elif data_type == "string":
|
||||
new.string_value = "" if new.is_null else value
|
||||
new.data_type = "string"
|
||||
|
||||
tool.Structural.import_boundary_condition_attributes(condition, props)
|
||||
props.active_boundary_condition = self.boundary_condition
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -212,7 +193,10 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_structural_boundary_condition"
|
||||
bl_label = "Edit Structural Boundary Condition"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
connection: bpy.props.IntProperty()
|
||||
connection: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
connection: int
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
@@ -223,18 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
connection = file.by_id(self.connection)
|
||||
condition = connection.AppliedCondition
|
||||
|
||||
attributes = {}
|
||||
for attribute in props.boundary_condition_attributes:
|
||||
if attribute.is_null:
|
||||
attributes[attribute.name] = {"value": None, "type": "null"}
|
||||
elif attribute.data_type == "string":
|
||||
attributes[attribute.name] = {"value": attribute.string_value, "type": "string"}
|
||||
elif attribute.enum_value == "IfcBoolean":
|
||||
attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value}
|
||||
else:
|
||||
attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value}
|
||||
|
||||
ifcopenshell.api.structural.edit_structural_boundary_condition(file, condition=condition, attributes=attributes)
|
||||
tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
|
||||
bpy.ops.bim.disable_editing_structural_boundary_condition()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -608,12 +581,14 @@ class EnableEditingStructuralLoadCase(bpy.types.Operator):
|
||||
self.props.active_load_case_id = self.load_case
|
||||
self.props.load_case_editing_type = "ATTRIBUTES"
|
||||
self.props.load_case_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(
|
||||
tool.Ifc.get().by_id(self.load_case), self.props.load_case_attributes, callback=self.import_attributes
|
||||
bonsai.bim.helper.import_attributes(
|
||||
tool.Ifc.get().by_id(self.load_case),
|
||||
self.props.load_case_attributes,
|
||||
callback=self.import_attributes_callback,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def import_attributes(self, name, prop, data):
|
||||
def import_attributes_callback(self, name: str, prop: object, data: object) -> None | Literal[False]:
|
||||
if name in ["SelfWeightCoefficients"]:
|
||||
return False
|
||||
|
||||
@@ -807,7 +782,7 @@ class EnableEditingStructuralLoad(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Structural.get_structural_props()
|
||||
props.structural_load_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(
|
||||
bonsai.bim.helper.import_attributes(
|
||||
tool.Ifc.get().by_id(self.structural_load), props.structural_load_attributes
|
||||
)
|
||||
props.active_structural_load_id = self.structural_load
|
||||
@@ -949,36 +924,15 @@ class EnableEditingBoundaryCondition(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_boundary_condition"
|
||||
bl_label = "Enable Editing Boundary Condition"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
boundary_condition: bpy.props.IntProperty()
|
||||
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
boundary_condition: int
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Structural.get_structural_props()
|
||||
props.boundary_condition_attributes.clear()
|
||||
|
||||
boundary_condition = tool.Ifc.get().by_id(self.boundary_condition)
|
||||
# bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data)
|
||||
schema = tool.Ifc.schema()
|
||||
for attribute in schema.declaration_by_name(boundary_condition.is_a()).all_attributes():
|
||||
value = getattr(boundary_condition, attribute.name(), None)
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
|
||||
new = props.boundary_condition_attributes.add()
|
||||
new.name = attribute.name()
|
||||
new.is_null = value is None
|
||||
new.is_optional = attribute.optional()
|
||||
if isinstance(data_type, tuple) and data_type[0] == "select":
|
||||
enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)]
|
||||
new.enum_items = json.dumps(enum_items)
|
||||
if isinstance(value, bool):
|
||||
new.bool_value = False if new.is_null else value
|
||||
new.data_type = "bool"
|
||||
new.enum_value = "IfcBoolean"
|
||||
elif isinstance(value, float):
|
||||
new.float_value = 0.0 if new.is_null else value
|
||||
new.data_type = "float"
|
||||
new.enum_value = next(i for i in enum_items if i != "IfcBoolean")
|
||||
elif data_type == "string":
|
||||
new.string_value = "" if new.is_null else value
|
||||
new.data_type = "string"
|
||||
props = tool.Structural.get_structural_props()
|
||||
tool.Structural.import_boundary_condition_attributes(boundary_condition, props)
|
||||
props.active_boundary_condition_id = self.boundary_condition
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1017,22 +971,8 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Structural.get_structural_props()
|
||||
self.file = tool.Ifc.get()
|
||||
# attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes)
|
||||
attributes = {}
|
||||
for attribute in props.boundary_condition_attributes:
|
||||
if attribute.is_null:
|
||||
attributes[attribute.name] = {"value": None, "type": "null"}
|
||||
elif attribute.data_type == "string":
|
||||
attributes[attribute.name] = {"value": attribute.string_value, "type": "string"}
|
||||
elif attribute.enum_value == "IfcBoolean":
|
||||
attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value}
|
||||
else:
|
||||
attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value}
|
||||
ifcopenshell.api.structural.edit_structural_boundary_condition(
|
||||
self.file,
|
||||
condition=self.file.by_id(props.active_boundary_condition_id),
|
||||
attributes=attributes,
|
||||
)
|
||||
ifc_file = tool.Ifc.get()
|
||||
condition = ifc_file.by_id(props.active_boundary_condition_id)
|
||||
tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
|
||||
bpy.ops.bim.load_boundary_conditions()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -79,7 +79,20 @@ def draw_boundary_condition_ui(
|
||||
def draw_boundary_condition_editable_ui(
|
||||
layout: bpy.types.UILayout, props: Union[BIMStructuralProperties, BIMObjectStructuralProperties]
|
||||
) -> None:
|
||||
draw_attributes(props.boundary_condition_attributes, layout)
|
||||
# Reimplement `draw_attributes` as we need to support multiple types for the same attribute.
|
||||
for attribute in props.boundary_condition_attributes:
|
||||
if attribute.data_type == "string":
|
||||
row = layout.row(align=True)
|
||||
bonsai.bim.helper.draw_attribute(attribute, row, enable_search=True)
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.prop(attribute, "enum_value", text=attribute["name"])
|
||||
if attribute.enum_value == "IfcBoolean":
|
||||
row.prop(attribute, "bool_value", text="")
|
||||
else:
|
||||
row.prop(attribute, "float_value", text="")
|
||||
if attribute.is_optional:
|
||||
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
|
||||
|
||||
def draw_boundary_condition_read_only_ui(layout: bpy.types.UILayout, boundary_condition: dict[str, Any]) -> None:
|
||||
@@ -605,6 +618,7 @@ class BIM_PT_boundary_conditions(Panel):
|
||||
BoundaryConditionsData.load()
|
||||
|
||||
self.props = tool.Structural.get_structural_props()
|
||||
assert self.layout
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(
|
||||
@@ -636,9 +650,8 @@ class BIM_PT_boundary_conditions(Panel):
|
||||
"active_boundary_condition_index",
|
||||
)
|
||||
|
||||
if self.props.active_boundary_condition_id:
|
||||
draw_boundary_condition_editable_ui(self.layout, self.props)
|
||||
# bonsai.bim.helper.draw_attributes(self.props.boundary_condition_attributes, self.layout)
|
||||
if self.props.active_boundary_condition_id:
|
||||
draw_boundary_condition_editable_ui(self.layout, self.props)
|
||||
|
||||
|
||||
class BIM_UL_boundary_conditions(UIList):
|
||||
|
||||
@@ -25,6 +25,7 @@ import ifcopenshell.api
|
||||
import ifcopenshell.api.style
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
from bonsai.bim.module.style.prop import ColourRgb
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from pathlib import Path
|
||||
from mathutils import Vector
|
||||
@@ -739,9 +740,10 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
|
||||
callback = None
|
||||
if self.ifc_class == "IfcSurfaceStyleLighting":
|
||||
|
||||
def callback(attribute_name, _, data):
|
||||
def callback(attribute_name: str, _: object, data: dict[str, Any]) -> None:
|
||||
assert attributes
|
||||
color = attributes.add()
|
||||
assert isinstance(color, ColourRgb)
|
||||
color.name = attribute_name
|
||||
color_value = data[attribute_name]
|
||||
if color_value is None:
|
||||
@@ -751,7 +753,7 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
|
||||
|
||||
if attributes is not None:
|
||||
attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback)
|
||||
bonsai.bim.helper.import_attributes(surface_style or self.ifc_class, attributes, callback)
|
||||
|
||||
material = tool.Ifc.get_object(style)
|
||||
msprops = tool.Style.get_material_style_props(material)
|
||||
|
||||
@@ -78,6 +78,7 @@ class Style(PropertyGroup):
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
ifc_definition_id: int
|
||||
total_elements: int
|
||||
style_classes: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
@@ -167,7 +168,6 @@ class ColourRgb(PropertyGroup):
|
||||
}
|
||||
|
||||
# to fit blender.bim.helper.draw_attribute
|
||||
is_uri = False
|
||||
is_optional = False
|
||||
special_type = ""
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class BIM_PT_styles(Panel):
|
||||
StylesData.load()
|
||||
|
||||
self.props = tool.Style.get_style_props()
|
||||
assert self.layout
|
||||
|
||||
if not self.props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
@@ -134,7 +135,7 @@ class BIM_PT_styles(Panel):
|
||||
# display style elements props during edit
|
||||
if self.props.is_editing_style:
|
||||
if self.props.is_editing_class == "IfcSurfaceStyle":
|
||||
bonsai.bim.helper.draw_attributes(self.props.attributes, self.layout)
|
||||
bonsai.bim.helper.draw_attributes(self.props.attributes, self.layout, enable_search=True)
|
||||
edit_label = "Save Attributes"
|
||||
elif self.props.is_editing_class == "IfcSurfaceStyleShading":
|
||||
self.draw_surface_style_shading()
|
||||
@@ -245,10 +246,10 @@ class BIM_PT_styles(Panel):
|
||||
op = row.operator("bim.browse_external_style", icon="APPEND_BLEND", text="Append From Blend File")
|
||||
style = self.props.active_style
|
||||
op.active_surface_style_id = style.ifc_definition_id
|
||||
bonsai.bim.helper.draw_attributes(self.props.external_style_attributes, self.layout)
|
||||
bonsai.bim.helper.draw_attributes(self.props.external_style_attributes, self.layout, enable_search=True)
|
||||
|
||||
def draw_refraction_surface_style(self):
|
||||
bonsai.bim.helper.draw_attributes(self.props.refraction_style_attributes, self.layout)
|
||||
bonsai.bim.helper.draw_attributes(self.props.refraction_style_attributes, self.layout, enable_search=True)
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
def draw_lighting_surface_style(self):
|
||||
|
||||
@@ -422,7 +422,7 @@ class EnableEditingZone(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.System.get_zone_props()
|
||||
props.attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.zone), props.attributes)
|
||||
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.zone), props.attributes)
|
||||
props.is_editing = self.zone
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -57,13 +57,13 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
related_objects = [bpy.data.objects[self.related_object]]
|
||||
else:
|
||||
related_objects = tool.Blender.get_selected_objects()
|
||||
model_props = tool.Model.get_model_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
for obj in related_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcObject"):
|
||||
continue
|
||||
core.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||
if model_props.occurrence_name_style == "TYPE":
|
||||
if prefs.occurrence_name_style == "TYPE":
|
||||
obj.name = tool.Model.generate_occurrence_name(relating_type, element.is_a())
|
||||
|
||||
|
||||
|
||||
@@ -42,11 +42,13 @@ from mathutils import Vector, Euler
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
from collections import namedtuple
|
||||
from typing import Union, TYPE_CHECKING
|
||||
from typing import Union, TYPE_CHECKING, Literal, get_args
|
||||
from collections.abc import Iterable
|
||||
from natsort import natsorted
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import MultipleFileSelect
|
||||
from bonsai.bim.prop import MultipleFileSelect, Attribute
|
||||
from bpy.stub_internal import rna_enums
|
||||
|
||||
|
||||
class SetTab(bpy.types.Operator):
|
||||
@@ -154,27 +156,20 @@ class SelectURIAttribute(bpy.types.Operator, ImportHelper):
|
||||
bl_label = "Select URI Attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select a local file"
|
||||
data_path: bpy.props.StringProperty(name="Data Path")
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
attribute_data_path: bpy.props.StringProperty(name="Data Path") # pyright: ignore[reportRedeclaration]
|
||||
"""Full data path to Attribute."""
|
||||
use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Use Relative Path",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
attribute_data_path: str
|
||||
use_relative_path: bool
|
||||
|
||||
def execute(self, context):
|
||||
# data_path contains the latter half of the path to the string_value property
|
||||
# I have no idea how to find out the former half, so let's just use brute force.
|
||||
data_path = self.data_path.replace(".string_value", "")
|
||||
attribute = None
|
||||
try:
|
||||
attribute = eval(f"bpy.context.scene.{data_path}")
|
||||
except:
|
||||
try:
|
||||
attribute = eval(f"bpy.context.active_object.{data_path}")
|
||||
except:
|
||||
try:
|
||||
attribute = eval(f"bpy.context.active_object.active_material.{data_path}")
|
||||
except:
|
||||
# Do you know a better way?
|
||||
pass
|
||||
if attribute:
|
||||
attribute.string_value = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
||||
attribute: Attribute = eval(self.attribute_data_path)
|
||||
attribute.string_value = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -240,19 +235,12 @@ class SelectDir(bpy.types.Operator, ImportHelper):
|
||||
bl_description = "Open a file browser to choose the directory"
|
||||
data_path: bpy.props.StringProperty(name="Data Path")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
data_path: str
|
||||
|
||||
def execute(self, context):
|
||||
crumbs = self.data_path.split(".")
|
||||
if crumbs[0] == "preferences":
|
||||
crumbs.pop(0)
|
||||
data = tool.Blender.get_addon_preferences()
|
||||
else:
|
||||
data = context
|
||||
while crumbs:
|
||||
crumb = crumbs.pop(0)
|
||||
if crumbs:
|
||||
data = getattr(data, crumb)
|
||||
else:
|
||||
setattr(data, crumb, os.path.dirname(self.filepath))
|
||||
data, attr = tool.Blender.resolve_data_path_to_data_attr(self.data_path)
|
||||
setattr(data, attr, os.path.dirname(self.filepath))
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -907,7 +895,7 @@ class FetchObjectPassport(bpy.types.Operator):
|
||||
context.active_object.data = bpy.data.meshes[reference.name]
|
||||
|
||||
|
||||
def update_enum_property_search_prop(self, context):
|
||||
def update_enum_property_search_prop(self: "BIM_OT_enum_property_search", context: bpy.types.Context) -> None:
|
||||
for i, prop in enumerate(self.collection_names):
|
||||
if prop.name == self.dummy_name:
|
||||
setattr(context.data, self.prop_name, self.collection_identifiers[i].name)
|
||||
@@ -916,6 +904,7 @@ def update_enum_property_search_prop(self, context):
|
||||
self.first_launch = False
|
||||
else:
|
||||
if not self.should_click_ok:
|
||||
# This closes popup immediately, avoiding the need to click "OK".
|
||||
context.window.screen = context.window.screen
|
||||
if predefined_type:
|
||||
try:
|
||||
@@ -930,6 +919,7 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
|
||||
bl_label = "Search"
|
||||
bl_description = "Search For Property"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
first_launch: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
|
||||
dummy_name: bpy.props.StringProperty(name="Property", update=update_enum_property_search_prop)
|
||||
collection_names: bpy.props.CollectionProperty(type=StrProperty)
|
||||
@@ -1299,3 +1289,170 @@ class ShowSystemInfo(bpy.types.Operator):
|
||||
|
||||
col.separator()
|
||||
col.label(text="(The information has been copied to the clipboard.)")
|
||||
|
||||
|
||||
def update_attribute_search_value(self: "BIM_OT_attribute_search_values", context: bpy.types.Context) -> None:
|
||||
should_click_ok = False
|
||||
attr_name, attribute_obj = BIM_OT_attribute_search_values.resolve_data_path(self.data_path)
|
||||
|
||||
value = self.search_value
|
||||
if self.data_type == "integer":
|
||||
value = int(value)
|
||||
elif self.data_type == "float":
|
||||
value = float(value)
|
||||
|
||||
setattr(attribute_obj, attr_name, value)
|
||||
|
||||
if self.first_launch:
|
||||
self.first_launch = False
|
||||
else:
|
||||
if not should_click_ok:
|
||||
# This closes popup immediately, avoiding the need to click "OK".
|
||||
context.window.screen = context.window.screen
|
||||
|
||||
|
||||
AttributeSearchDataType = Literal["string", "integer", "float"]
|
||||
|
||||
|
||||
class BIM_OT_attribute_search_values(bpy.types.Operator):
|
||||
"""Search for attribute values. This implementation is based on bim.enum_property_search"""
|
||||
|
||||
bl_idname = "bim.attribute_search_values"
|
||||
bl_label = "Search Attribute Values"
|
||||
bl_description = "Search for attribute values used in the elements of the same IFC class."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
# Required properties.
|
||||
attribute_name: bpy.props.StringProperty(name="Attribute Name")
|
||||
attribute_ifc_class: bpy.props.StringProperty(name="Attribute IFC Class")
|
||||
data_path: bpy.props.StringProperty(name="Data Path")
|
||||
data_type: bpy.props.EnumProperty(
|
||||
name="Data Type",
|
||||
items=[(i, i, "") for i in get_args(AttributeSearchDataType)],
|
||||
)
|
||||
|
||||
# Internal properties.
|
||||
first_launch: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
|
||||
search_value: bpy.props.StringProperty(
|
||||
name="Search",
|
||||
description="Search for attribute values",
|
||||
update=update_attribute_search_value,
|
||||
default="",
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
collection_values: bpy.props.CollectionProperty(type=StrProperty, options={"SKIP_SAVE"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
first_launch: bool
|
||||
attribute_name: str
|
||||
attribute_ifc_class: str
|
||||
data_path: str
|
||||
data_type: AttributeSearchDataType
|
||||
search_value: str
|
||||
collection_values: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
|
||||
@staticmethod
|
||||
def resolve_data_path(data_path: str) -> tuple[str, "Attribute"]:
|
||||
"""Resolve the data path of an object's attribute to get the attribute name and the object."""
|
||||
attribute_obj, _, attr_name = data_path.rpartition(".")
|
||||
attribute_obj = eval(attribute_obj)
|
||||
return attr_name, attribute_obj
|
||||
|
||||
def invoke(self, context, event):
|
||||
required_props = (self.attribute_name, self.attribute_ifc_class, self.data_path)
|
||||
assert all(required_props), required_props
|
||||
|
||||
attr_name, attribute_obj = self.resolve_data_path(self.data_path)
|
||||
self.search_value = str(getattr(attribute_obj, attr_name, ""))
|
||||
|
||||
unique_values = self.get_unique_attribute_values()
|
||||
string_values = natsorted(unique_values)
|
||||
|
||||
for value in string_values:
|
||||
self.collection_values.add().name = value
|
||||
|
||||
assert context.window_manager
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def get_unique_attribute_values(self) -> list[str]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
unique_values: set[str] = set()
|
||||
ifc_class = self.attribute_ifc_class
|
||||
|
||||
elements = ifc_file.by_type(ifc_class, include_subtypes=True)
|
||||
|
||||
for element in elements:
|
||||
# We check just direct entity attributes and simply check if the attribute exists
|
||||
if hasattr(element, self.attribute_name):
|
||||
value = getattr(element, self.attribute_name)
|
||||
if value is not None:
|
||||
unique_values.add(str(value))
|
||||
|
||||
return list(unique_values)
|
||||
|
||||
def draw(self, context) -> None:
|
||||
assert self.layout
|
||||
row = self.layout.row()
|
||||
row.label(text=f"Select {self.attribute_name} value:")
|
||||
row = self.layout.row()
|
||||
row.prop_search(
|
||||
self,
|
||||
"search_value",
|
||||
self,
|
||||
"collection_values",
|
||||
text="",
|
||||
results_are_suggestions=True,
|
||||
)
|
||||
|
||||
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_attribute_add_subitem(bpy.types.Operator):
|
||||
bl_idname = "bim.attribute_add_subitem"
|
||||
bl_label = "Add Subitem"
|
||||
bl_description = "Add subitem to the current attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""Full data path."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
data_path: str
|
||||
|
||||
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
col: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
col = eval(self.data_path)
|
||||
|
||||
attr: Attribute = col.data
|
||||
if attr.is_optional and not col:
|
||||
attr.is_null = False
|
||||
|
||||
col.add()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
|
||||
bl_idname = "bim.attribute_remove_subitem"
|
||||
bl_label = "Add Subitem"
|
||||
bl_description = "Add subitem to the current attribute"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
||||
"""Full data path."""
|
||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
data_path: str
|
||||
index: int
|
||||
|
||||
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
col: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
col = eval(self.data_path)
|
||||
col.remove(self.index)
|
||||
|
||||
attr: Attribute = col.data
|
||||
if attr.is_optional and not col:
|
||||
attr.is_null = True
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
import platformdirs
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.pset
|
||||
import ifcopenshell.util.unit
|
||||
@@ -128,20 +127,6 @@ def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None
|
||||
bonsai.bim.schema.ifc.schema_dir = bim_props.schema_dir
|
||||
|
||||
|
||||
def update_data_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
|
||||
import bonsai.bim.schema
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
bonsai.bim.schema.ifc.data_dir = bim_props.data_dir
|
||||
|
||||
|
||||
def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
|
||||
import bonsai.bim.schema
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir
|
||||
|
||||
|
||||
def update_section_color(self: "BIMProperties", context: bpy.types.Context) -> None:
|
||||
section_node_group = bpy.data.node_groups.get("Section Override")
|
||||
if section_node_group is None:
|
||||
@@ -234,7 +219,9 @@ def update_attribute_value(self: "Attribute", context: bpy.types.Context) -> Non
|
||||
|
||||
def update_is_null(self: "Attribute", context: bpy.types.Context) -> None:
|
||||
if self.is_null:
|
||||
if self.data_type != "enum" and self.get_value() != (default := self.get_value_default()):
|
||||
if self.data_type == "list[string]":
|
||||
self.subitems_values.clear()
|
||||
elif self.data_type != "enum" and self.get_value() != (default := self.get_value_default()):
|
||||
self.set_value(default)
|
||||
if self.is_null is not True:
|
||||
self.is_null = True
|
||||
@@ -286,8 +273,8 @@ def get_display_name(self: "Attribute") -> str:
|
||||
return f"{name}, {unit_symbol}"
|
||||
|
||||
|
||||
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file"]
|
||||
AttributeSpecialType = Literal["", "DATE", "DATETIME", "LENGTH", "AREA", "VOLUME", "FORCE", "LOGICAL"]
|
||||
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
|
||||
AttributeSpecialType = Literal["", "DATE", "DATETIME", "LENGTH", "AREA", "VOLUME", "FORCE", "LOGICAL", "URI"]
|
||||
|
||||
|
||||
class Attribute(PropertyGroup):
|
||||
@@ -300,6 +287,8 @@ class Attribute(PropertyGroup):
|
||||
name="Data Type",
|
||||
items=[(i, i, "") for i in get_args(AttributeDataType)],
|
||||
)
|
||||
|
||||
# Value containers.
|
||||
string_value: StringProperty(name="Value", update=update_attribute_value, description=tooltip)
|
||||
bool_value: BoolProperty(name="Value", update=update_attribute_value, description=tooltip)
|
||||
int_value: IntProperty(
|
||||
@@ -330,14 +319,17 @@ class Attribute(PropertyGroup):
|
||||
filepath_value: PointerProperty(type=MultipleFileSelect)
|
||||
filter_glob: StringProperty()
|
||||
is_null: BoolProperty(name="Is Null", update=update_is_null)
|
||||
is_optional: BoolProperty(name="Is Optional")
|
||||
is_uri: BoolProperty(name="Is Uri", default=False)
|
||||
is_selected: BoolProperty(name="Is Selected", default=False)
|
||||
subitems_values: CollectionProperty(type=StrProperty) # pyright: ignore[reportRedeclaration]
|
||||
|
||||
# Attribute parameters.
|
||||
is_optional: BoolProperty(name="Is Optional")
|
||||
value_min: FloatProperty(description="This is used to validate int_value and float_value")
|
||||
value_min_constraint: BoolProperty(default=False, description="True if the numerical value has a lower bound")
|
||||
value_max: FloatProperty(description="This is used to validate int_value and float_value")
|
||||
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
|
||||
special_type: StringProperty(name="Special Value Type", default="")
|
||||
use_explorer_ui: BoolProperty() # pyright: ignore[reportRedeclaration]
|
||||
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
|
||||
update: StringProperty(name="Update", description="Custom update function to be executed")
|
||||
|
||||
@@ -360,23 +352,28 @@ class Attribute(PropertyGroup):
|
||||
filepath_value: MultipleFileSelect
|
||||
filter_glob: str
|
||||
is_null: bool
|
||||
is_optional: bool
|
||||
is_uri: bool
|
||||
is_selected: bool
|
||||
subitems_values: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
|
||||
is_optional: bool
|
||||
value_min: float
|
||||
value_min_constraint: bool
|
||||
value_max: float
|
||||
value_max_constraint: bool
|
||||
use_explorer_ui: bool
|
||||
metadata: str
|
||||
update: str
|
||||
|
||||
def get_value(self) -> Union[str, float, int, bool, None]:
|
||||
def get_value(self) -> Union[str, float, int, bool, list[str], None]:
|
||||
if self.is_optional and self.is_null:
|
||||
return None
|
||||
if self.data_type == "string":
|
||||
return self.string_value.replace("\\n", "\n")
|
||||
if self.data_type == "file":
|
||||
return [f.name for f in self.filepath_value.file_list]
|
||||
elif self.data_type == "list[string]":
|
||||
return [s.name for s in self.subitems_values]
|
||||
|
||||
value_name = self.get_value_name()
|
||||
if value_name == "enum_value":
|
||||
value = tool.Blender.get_enum_safe(self, "enum_value")
|
||||
@@ -387,7 +384,7 @@ class Attribute(PropertyGroup):
|
||||
value = value == "TRUE"
|
||||
return value
|
||||
|
||||
def get_value_default(self) -> Union[str, float, int, bool]:
|
||||
def get_value_default(self) -> Union[str, float, int, bool, list[str]]:
|
||||
data_type = self.data_type
|
||||
if data_type == "string":
|
||||
return ""
|
||||
@@ -401,10 +398,12 @@ class Attribute(PropertyGroup):
|
||||
return "0"
|
||||
elif data_type == "file":
|
||||
return ""
|
||||
elif data_type == "list[string]":
|
||||
return []
|
||||
else:
|
||||
assert_never(data_type)
|
||||
|
||||
def get_value_name(self, display_only: bool = False) -> str:
|
||||
def get_value_name(self, display_only: bool = False):
|
||||
"""Get name of the value attribute.
|
||||
|
||||
:param display_only: Should be `True` if the value won't be accessed directly
|
||||
@@ -425,6 +424,8 @@ class Attribute(PropertyGroup):
|
||||
return "enum_value"
|
||||
elif data_type == "file":
|
||||
return "filepath_value"
|
||||
elif data_type == "list[string]":
|
||||
return "subitems_values"
|
||||
else:
|
||||
assert_never(data_type)
|
||||
|
||||
@@ -505,16 +506,7 @@ class BIMProperties(PropertyGroup):
|
||||
schema_dir: StringProperty(
|
||||
default=os.path.join(cwd, "schema") + os.path.sep, name="Schema Directory", update=update_schema_dir
|
||||
)
|
||||
data_dir: StringProperty(
|
||||
default=(platformdirs.user_data_path("bonsai", roaming=True, ensure_exists=True) / "data").__str__(),
|
||||
name="Data Directory",
|
||||
update=update_data_dir,
|
||||
)
|
||||
cache_dir: StringProperty(
|
||||
default=platformdirs.user_cache_dir("bonsai"), name="Cache Directory", update=update_cache_dir
|
||||
)
|
||||
has_blend_warning: BoolProperty(name="Has Blend Warning", default=False)
|
||||
pset_dir: StringProperty(default=os.path.join("psets") + os.path.sep, name="Default Psets Directory")
|
||||
ifc_file: StringProperty(name="IFC File")
|
||||
last_transaction: StringProperty(name="Last Transaction")
|
||||
should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False)
|
||||
@@ -570,8 +562,6 @@ class BIMProperties(PropertyGroup):
|
||||
if TYPE_CHECKING:
|
||||
is_dirty: bool
|
||||
schema_dir: str
|
||||
data_dir: str
|
||||
cache_dir: str
|
||||
has_blend_warning: bool
|
||||
pset_dir: str
|
||||
ifc_file: str
|
||||
@@ -605,16 +595,12 @@ class PsetQto(PropertyGroup):
|
||||
is_editable: BoolProperty(name="Is Editable")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
properties: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
is_expanded: bool
|
||||
is_editable: bool
|
||||
|
||||
|
||||
class GlobalId(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
|
||||
class BIMCollectionProperties(PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ import bpy_restrict_state
|
||||
|
||||
|
||||
class IfcSchema:
|
||||
data_dir: str
|
||||
cache_dir: str
|
||||
schema_dir: str
|
||||
|
||||
def __init__(self, schema_identifier="IFC4"):
|
||||
if schema_identifier not in ("IFC2X3", "IFC4", "IFC4X3_ADD2"):
|
||||
schema_identifier = "IFC4"
|
||||
|
||||
+190
-21
@@ -19,6 +19,7 @@
|
||||
import os
|
||||
import bpy
|
||||
import platform
|
||||
import platformdirs
|
||||
import bonsai.bim.helper
|
||||
from pathlib import Path
|
||||
from bpy.types import Panel
|
||||
@@ -35,11 +36,13 @@ import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from ifcopenshell.util.file import IfcHeaderExtractor
|
||||
from bonsai.bim.prop import Attribute
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import Optional, TYPE_CHECKING, Literal
|
||||
from natsort import natsorted
|
||||
|
||||
|
||||
class IFCFileSelector:
|
||||
layout: bpy.types.UILayout
|
||||
|
||||
# Avoid overriding blender prop annotations at runtime.
|
||||
if TYPE_CHECKING:
|
||||
filepath: str
|
||||
@@ -76,6 +79,7 @@ class IFCFileSelector:
|
||||
assert isinstance(context.space_data, bpy.types.SpaceFileBrowser)
|
||||
# Access filepath & Directory https://blender.stackexchange.com/a/207665
|
||||
params = context.space_data.params
|
||||
assert params
|
||||
# Decode byte string https://stackoverflow.com/a/47737082/
|
||||
directory = Path(params.directory.decode("utf-8"))
|
||||
filepath = os.path.join(directory, params.filename)
|
||||
@@ -122,6 +126,7 @@ class BIM_PT_section_plane(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_sandbox"
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
props = tool.Blender.get_bim_props()
|
||||
@@ -145,6 +150,7 @@ class BIM_PT_section_with_cappings(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_sandbox"
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
wm = context.window_manager
|
||||
row = layout.row(align=True)
|
||||
@@ -189,7 +195,16 @@ class BIM_UL_clipping_plane(bpy.types.UIList):
|
||||
|
||||
|
||||
class BIM_UL_generic(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
def draw_item(
|
||||
self,
|
||||
context,
|
||||
layout: bpy.types.UILayout,
|
||||
data,
|
||||
item: bpy.types.PropertyGroup,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
) -> None:
|
||||
if item:
|
||||
layout.prop(item, "name", text="", emboss=False)
|
||||
else:
|
||||
@@ -205,6 +220,98 @@ class BIM_UL_topics(bpy.types.UIList):
|
||||
layout.label(text="", translate=False)
|
||||
|
||||
|
||||
class DocPreferences(bpy.types.PropertyGroup):
|
||||
sheets_dir: StringProperty(
|
||||
default=os.path.join("sheets") + os.path.sep,
|
||||
name="Default Sheets Directory",
|
||||
)
|
||||
layouts_dir: StringProperty(
|
||||
default=os.path.join("layouts") + os.path.sep,
|
||||
name="Default Layouts Directory",
|
||||
)
|
||||
titleblocks_dir: StringProperty(
|
||||
default=os.path.join("layouts", "titleblocks") + os.path.sep,
|
||||
name="Default Titleblocks Directory",
|
||||
)
|
||||
drawings_dir: StringProperty(
|
||||
default=os.path.join("drawings") + os.path.sep,
|
||||
name="Default Drawings Directory",
|
||||
)
|
||||
stylesheet_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "default.css"),
|
||||
name="Default Stylesheet",
|
||||
)
|
||||
schedules_stylesheet_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "schedule.css"),
|
||||
name="Default Stylesheet for Schedules",
|
||||
)
|
||||
markers_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "markers.svg"),
|
||||
name="Default Markers",
|
||||
)
|
||||
symbols_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "symbols.svg"),
|
||||
name="Default Symbols",
|
||||
)
|
||||
patterns_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "patterns.svg"),
|
||||
name="Default Patterns",
|
||||
)
|
||||
shadingstyles_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "shading_styles.json"),
|
||||
name="Default Shading Styles",
|
||||
)
|
||||
shadingstyle_default: StringProperty(
|
||||
default="Blender Default",
|
||||
name="Default Shading Style",
|
||||
)
|
||||
drawing_font: StringProperty(
|
||||
default="OpenGost Type B TT.ttf",
|
||||
name="Drawing Font",
|
||||
)
|
||||
magic_font_scale: bpy.props.FloatProperty(
|
||||
default=0.004118616,
|
||||
name="Font Scale Factor",
|
||||
)
|
||||
imperial_precision: StringProperty(
|
||||
default="1/32",
|
||||
name="Imperial Precision",
|
||||
)
|
||||
tolerance: bpy.props.FloatProperty(
|
||||
default=0.00001,
|
||||
name="A tolerance used when selecting objects",
|
||||
)
|
||||
classes_to_wireframe: StringProperty(
|
||||
default="IfcVirtualElement",
|
||||
name="Classes to Wireframe",
|
||||
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
|
||||
)
|
||||
classes_no_cut: StringProperty(
|
||||
default="IfcVirtualElement, IfcSpace",
|
||||
name="Classes that are not cut",
|
||||
description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
sheets_dir: str
|
||||
layouts_dir: str
|
||||
titleblocks_dir: str
|
||||
drawings_dir: str
|
||||
stylesheet_path: str
|
||||
schedules_stylesheet_path: str
|
||||
markers_path: str
|
||||
symbols_path: str
|
||||
patterns_path: str
|
||||
shadingstyles_path: str
|
||||
shadingstyle_default: str
|
||||
drawing_font: str
|
||||
magic_font_scale: float
|
||||
imperial_precision: str
|
||||
tolerance: float
|
||||
classes_to_wireframe: str
|
||||
classes_no_cut: str
|
||||
|
||||
|
||||
class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
bl_idname = tool.Blender.get_blender_addon_package_name()
|
||||
svg2pdf_command: StringProperty(
|
||||
@@ -320,6 +427,53 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
description="When modifying openings, other elements of the model will display with some transparency.\n0 is fully transparent and 100 is fully opaque",
|
||||
)
|
||||
|
||||
bsdd_load_preview_dictionaries: BoolProperty(
|
||||
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
|
||||
)
|
||||
bsdd_load_inactive_dictionaries: BoolProperty(
|
||||
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
|
||||
)
|
||||
bsdd_load_test_dictionaries: BoolProperty(
|
||||
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
|
||||
)
|
||||
should_disable_undo_on_save: BoolProperty(
|
||||
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
|
||||
)
|
||||
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
|
||||
occurrence_name_style: bpy.props.EnumProperty(
|
||||
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
|
||||
name="Occurrence Name Style",
|
||||
)
|
||||
occurrence_name_function: bpy.props.StringProperty(
|
||||
name="Occurrence Name Function",
|
||||
description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style",
|
||||
)
|
||||
|
||||
def update_data_dir(self, context: bpy.types.Context) -> None:
|
||||
import bonsai.bim.schema
|
||||
|
||||
bonsai.bim.schema.ifc.data_dir = self.data_dir
|
||||
|
||||
def update_cache_dir(self, context: bpy.types.Context) -> None:
|
||||
import bonsai.bim.schema
|
||||
|
||||
bonsai.bim.schema.ifc.cache_dir = self.cache_dir
|
||||
|
||||
data_dir: StringProperty(
|
||||
default=(platformdirs.user_data_path("bonsai", roaming=True, ensure_exists=True) / "data").__str__(),
|
||||
name="Data Directory",
|
||||
update=update_data_dir,
|
||||
)
|
||||
cache_dir: StringProperty(
|
||||
default=platformdirs.user_cache_dir("bonsai"), name="Cache Directory", update=update_cache_dir
|
||||
)
|
||||
|
||||
pset_dir: StringProperty(
|
||||
default=os.path.join("psets") + os.path.sep,
|
||||
name="Default Psets Directory",
|
||||
)
|
||||
doc: bpy.props.PointerProperty(type=DocPreferences)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
svg2pdf_command: str
|
||||
svg2dxf_command: str
|
||||
@@ -341,6 +495,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
decorator_color_error: tuple[float, float, float, float]
|
||||
decorator_color_background: tuple[float, float, float, float]
|
||||
opening_focus_opacity: int
|
||||
bsdd_load_preview_dictionaries: bool
|
||||
bsdd_load_inactive_dictionaries: bool
|
||||
bsdd_load_test_dictionaries: bool
|
||||
should_disable_undo_on_save: bool
|
||||
should_stream: bool
|
||||
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
|
||||
occurrence_name_function: str
|
||||
data_dir: str
|
||||
cache_dir: str
|
||||
pset_dir: str
|
||||
doc: DocPreferences
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
@@ -381,29 +546,26 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
layout.prop(self, "spatial_elements_unselectable")
|
||||
|
||||
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
props = tool.Model.get_model_props()
|
||||
layout.prop(props, "occurrence_name_style")
|
||||
if props.occurrence_name_style == "CUSTOM":
|
||||
layout.prop(props, "occurrence_name_function")
|
||||
layout.prop(self, "occurrence_name_style")
|
||||
if self.occurrence_name_style == "CUSTOM":
|
||||
layout.prop(self, "occurrence_name_function")
|
||||
|
||||
def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
props = tool.Blender.get_bim_props()
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "data_dir")
|
||||
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir"
|
||||
row.prop(self, "data_dir")
|
||||
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.data_dir"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "cache_dir")
|
||||
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir"
|
||||
row.prop(self, "cache_dir")
|
||||
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.cache_dir"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "tmp_dir")
|
||||
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir"
|
||||
|
||||
def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
props = tool.Blender.get_bim_props()
|
||||
layout.prop(props, "pset_dir")
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
layout.prop(self, "pset_dir")
|
||||
dprops = self.doc
|
||||
layout.prop(dprops, "sheets_dir")
|
||||
layout.prop(dprops, "layouts_dir")
|
||||
layout.prop(dprops, "titleblocks_dir")
|
||||
@@ -433,14 +595,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
|
||||
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "opening_focus_opacity")
|
||||
props = tool.Project.get_project_props()
|
||||
layout.prop(props, "should_disable_undo_on_save")
|
||||
layout.prop(props, "should_stream")
|
||||
bprops = tool.Bsdd.get_bsdd_props()
|
||||
layout.prop(self, "should_disable_undo_on_save")
|
||||
layout.prop(self, "should_stream")
|
||||
layout.label(text="bSDD:")
|
||||
layout.prop(bprops, "load_preview_dictionaries")
|
||||
layout.prop(bprops, "load_inactive_dictionaries")
|
||||
layout.prop(bprops, "load_test_dictionaries")
|
||||
layout.prop(self, "bsdd_load_preview_dictionaries")
|
||||
layout.prop(self, "bsdd_load_inactive_dictionaries")
|
||||
layout.prop(self, "bsdd_load_test_dictionaries")
|
||||
|
||||
|
||||
# Scene panel groups
|
||||
@@ -1186,6 +1346,13 @@ class EnumData:
|
||||
organizations = tool.Ifc.get().by_type("IfcOrganization")
|
||||
return natsorted(((str(e.id()), e.Name, "") for e in organizations), key=lambda x: x[1])
|
||||
|
||||
@classmethod
|
||||
def postal_addresses(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
addresses = tool.Ifc.get().by_type("IfcPostalAddress")
|
||||
items = ((str(e.id()), (e.Description or "Undescribed"), "") for e in addresses)
|
||||
items = natsorted(items, key=lambda x: x[1])
|
||||
return items
|
||||
|
||||
|
||||
class UIData:
|
||||
data = {}
|
||||
@@ -1334,6 +1501,8 @@ class BIM_PT_decorators_overlay(Panel):
|
||||
row = col.row(align=True)
|
||||
row.prop(model_props, "show_slab_direction", text="Slab Direction")
|
||||
row = col.row(align=True)
|
||||
row.prop(model_props, "show_bounding_box", text="Bounding Box Dimensions")
|
||||
row = col.row(align=True)
|
||||
row.prop(model_props, "show_cut_decorator", text="Cut Decorator")
|
||||
row.prop(model_props, "show_cut_decorator_fill", text="Fill Cut Decorator")
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ def add_context(
|
||||
context_type: Optional[str] = None,
|
||||
context_identifier: Optional[str] = None,
|
||||
target_view: Optional[str] = None,
|
||||
target_scale: Optional[float] = None,
|
||||
parent: Optional[str] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
return ifc.run(
|
||||
@@ -38,7 +37,6 @@ def add_context(
|
||||
context_type=context_type,
|
||||
context_identifier=context_identifier,
|
||||
target_view=target_view,
|
||||
target_scale=target_scale,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
|
||||
@@ -340,9 +340,10 @@ def select_cost_schedule_products(
|
||||
|
||||
|
||||
def import_cost_schedule_csv(
|
||||
cost: type[tool.Cost], file_path: str, is_schedule_of_rates: bool
|
||||
cost: type[tool.Cost], resolved_path: str, is_schedule_of_rates: bool
|
||||
) -> ifcopenshell.entity_instance:
|
||||
cost_schedule = cost.import_cost_schedule_csv(file_path, is_schedule_of_rates)
|
||||
|
||||
cost_schedule = cost.import_cost_schedule_csv(resolved_path, is_schedule_of_rates)
|
||||
return cost_schedule
|
||||
|
||||
|
||||
@@ -354,12 +355,18 @@ def remove_csv_filepath(cost: type[tool.Cost], cost_schedule) -> None:
|
||||
cost.remove_csv_filepath(cost_schedule)
|
||||
|
||||
|
||||
def refresh_cost_schedule_csv(ifc: type[tool.Ifc], cost: type[tool.Cost]) -> None:
|
||||
def refresh_cost_schedule_csv(cost: type[tool.Cost]) -> Optional[str]:
|
||||
cost.delete_all_cost_items()
|
||||
cost.refresh_cost_schedule_csv()
|
||||
cost.load_cost_schedule_tree()
|
||||
|
||||
|
||||
def remove_cost_schedule_csv_link(cost: type[tool.Cost]) -> None:
|
||||
cost_schedule = cost.get_active_cost_schedule()
|
||||
if cost_schedule:
|
||||
cost.remove_csv_filepath(cost_schedule)
|
||||
|
||||
|
||||
def add_cost_column(cost: type[tool.Cost], name: str) -> None:
|
||||
cost.add_cost_column(name)
|
||||
|
||||
@@ -396,6 +403,13 @@ def export_cost_schedules(
|
||||
return cost.export_cost_schedules(dirpath, format, cost_schedule)
|
||||
|
||||
|
||||
def export_cost_schedules_to_pdf(
|
||||
cost: type[tool.Cost], filepath: str, cost_schedule: ifcopenshell.entity_instance, options: dict
|
||||
):
|
||||
cost.play_sound()
|
||||
return cost.export_cost_schedules_to_pdf(filepath, cost_schedule, options)
|
||||
|
||||
|
||||
def clear_cost_item_assignments(
|
||||
ifc: type[tool.Ifc],
|
||||
cost: type[tool.Cost],
|
||||
|
||||
@@ -51,7 +51,7 @@ def add_instance_flooring_covering_from_cursor(
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0)
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
|
||||
name = "Covering"
|
||||
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
|
||||
|
||||
@@ -65,7 +65,6 @@ def add_instance_flooring_covering_from_cursor(
|
||||
|
||||
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
|
||||
body = spatial.get_body_representation(obj)
|
||||
print(obj, body)
|
||||
spatial.regen_obj_representation(obj, body)
|
||||
|
||||
|
||||
@@ -96,7 +95,7 @@ def add_instance_ceiling_covering_from_cursor(
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0)
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
|
||||
name = "Covering"
|
||||
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
|
||||
|
||||
@@ -128,7 +127,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0)
|
||||
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
|
||||
|
||||
name = "Aux"
|
||||
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
|
||||
@@ -152,7 +151,7 @@ def add_instance_flooring_coverings_from_walls(root: type[tool.Root], spatial: t
|
||||
union = spatial.get_union_shape_from_selected_objects()
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h=0)
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
|
||||
|
||||
name = "Covering" + str(i)
|
||||
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
|
||||
@@ -180,7 +179,7 @@ def add_instance_ceiling_coverings_from_walls(
|
||||
union = spatial.get_union_shape_from_selected_objects()
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h=0)
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
|
||||
|
||||
name = "Covering" + str(i)
|
||||
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union, Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
@@ -108,13 +108,22 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
|
||||
drawing.import_sheets()
|
||||
|
||||
|
||||
def regenerate_sheet(drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance) -> None:
|
||||
def regenerate_sheet(
|
||||
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
|
||||
) -> Union[list[tool.Drawing.SheetWarningType], None]:
|
||||
warnings = drawing.validate_sheet_files(sheet)
|
||||
if warnings:
|
||||
return warnings
|
||||
|
||||
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
|
||||
assert titleblock_uri
|
||||
|
||||
drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem))
|
||||
try:
|
||||
drawing.add_drawings(sheet)
|
||||
except FileNotFoundError:
|
||||
path_layout = drawing.get_document_uri(sheet, "LAYOUT")
|
||||
assert path_layout
|
||||
if drawing.does_file_exist(path_layout):
|
||||
drawing.delete_file(path_layout)
|
||||
|
||||
|
||||
@@ -110,10 +110,15 @@ def select_container(
|
||||
spatial.set_active_object(ifc.get_object(container), selection_mode=selection_mode)
|
||||
|
||||
|
||||
def select_similar_container(ifc: type[tool.Ifc], spatial: type[tool.Spatial], obj: bpy.types.Object) -> None:
|
||||
def select_similar_container(
|
||||
ifc: type[tool.Ifc],
|
||||
spatial: type[tool.Spatial],
|
||||
obj: bpy.types.Object,
|
||||
is_recursive: bool = True,
|
||||
) -> None:
|
||||
element = ifc.get_entity(obj)
|
||||
if element:
|
||||
spatial.select_products(spatial.get_decomposed_elements(spatial.get_container(element)))
|
||||
spatial.select_products(spatial.get_decomposed_elements(spatial.get_container(element), is_recursive))
|
||||
|
||||
|
||||
def select_product(spatial: type[tool.Spatial], product: ifcopenshell.entity_instance) -> None:
|
||||
@@ -212,6 +217,7 @@ def generate_space(
|
||||
if element and element.is_a("IfcSpace"):
|
||||
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
|
||||
spatial.edit_active_space_obj_from_mesh(mesh)
|
||||
spatial.translate_obj_to_z_location(active_obj, z)
|
||||
else:
|
||||
if relating_type:
|
||||
name = model.generate_occurrence_name(relating_type, "IfcSpace")
|
||||
@@ -242,7 +248,7 @@ def generate_spaces_from_walls(
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
|
||||
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h)
|
||||
bm = spatial.get_bmesh_from_polygon(poly, h, polygon_is_si=False)
|
||||
|
||||
name = "Space" + str(i)
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@ class Aggregate:
|
||||
def get_relating_object(cls, related_element): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Attribute:
|
||||
pass
|
||||
|
||||
|
||||
@interface
|
||||
class Bcf:
|
||||
pass
|
||||
@@ -223,6 +228,7 @@ class Cost:
|
||||
def expand_cost_item(cls, cost_item_id): pass
|
||||
def expand_cost_items(cls): pass
|
||||
def export_cost_schedules(cls, filepath, format, cost_schedule): pass
|
||||
def export_cost_schedules_to_pdf(cls, filepath, cost_schedule, options): pass
|
||||
def format_unit(cls, unit): pass
|
||||
def get_active_cost_item(cls): pass
|
||||
def get_active_cost_schedule(cls): pass
|
||||
@@ -537,7 +543,6 @@ class Material:
|
||||
def ensure_material_assigned(cls, elements, material_type, material): pass
|
||||
def ensure_material_unassigned(cls, elements): pass
|
||||
def ensure_new_material_set_is_valid(cls, material): pass
|
||||
def get_active_material_item(cls): pass
|
||||
def get_active_material_type(cls): pass
|
||||
def get_default_material(cls): pass
|
||||
def get_elements_by_material(cls, material): pass
|
||||
@@ -927,7 +932,7 @@ class Spatial:
|
||||
def filter_products(cls, products, action): pass
|
||||
def get_active_container(cls): pass
|
||||
def get_container(cls, element): pass
|
||||
def get_decomposed_elements(cls, container): pass
|
||||
def get_decomposed_elements(cls, container, recursive): pass
|
||||
def get_object_matrix(cls, obj): pass
|
||||
def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass
|
||||
def get_selected_product_types(cls): pass
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from bonsai.tool.aggregate import Aggregate
|
||||
from bonsai.tool.attribute import Attribute
|
||||
from bonsai.tool.bcf import Bcf
|
||||
from bonsai.tool.blender import Blender
|
||||
from bonsai.tool.boundary import Boundary
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 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/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.core.tool
|
||||
import bonsai.bim.helper as helper
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
Union,
|
||||
Literal,
|
||||
TypeVar,
|
||||
TYPE_CHECKING,
|
||||
assert_never,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.attribute.prop import BIMAttributeProperties, BIMExplorerProperties
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Attribute(bonsai.core.tool.Attribute):
|
||||
@classmethod
|
||||
def get_explorer_props(cls) -> BIMExplorerProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMExplorerProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def refresh_uilist_entities(cls) -> None:
|
||||
props = cls.get_explorer_props()
|
||||
props.entities.clear()
|
||||
ifc_file = tool.Ifc.get()
|
||||
for element in ifc_file.by_type(props.ifc_class):
|
||||
entity = props.entities.add()
|
||||
entity.name = str(element)
|
||||
entity.ifc_definition_id = element.id()
|
||||
|
||||
@classmethod
|
||||
def enable_editing_entity(cls, entity: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_explorer_props()
|
||||
props.editing_entity_id = entity.id()
|
||||
|
||||
@classmethod
|
||||
def disable_editing_entity(cls) -> None:
|
||||
props = cls.get_explorer_props()
|
||||
props.entity_attributes.clear()
|
||||
props.property_unset("editing_entity_id")
|
||||
|
||||
@classmethod
|
||||
def import_entity_attributes(cls, entity: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_explorer_props()
|
||||
helper.import_attributes(entity, props.entity_attributes)
|
||||
|
||||
@classmethod
|
||||
def export_entity_attributes(cls) -> dict[str, Any]:
|
||||
props = cls.get_explorer_props()
|
||||
attributes = helper.export_attributes(props.entity_attributes)
|
||||
return attributes
|
||||
|
||||
EXPLORER_UI_LOOKUP_PROPS: dict[str, dict[str, str]] = {
|
||||
"IfcSite": {"SiteAddress": "postal_addresses"},
|
||||
"IfcBuilding": {"BuildingAddress": "postal_addresses"},
|
||||
}
|
||||
"""``ifc_class -> {attribute_name -> enum_items_dynamic}``"""
|
||||
|
||||
@classmethod
|
||||
def does_ifc_class_support_explorer_lookup(cls, ifc_class: str) -> Union[dict[str, str], None]:
|
||||
"""
|
||||
:return: ``{attribute_name -> enum_items_dynamic}``
|
||||
if IFC class does support explorer UI lookup, otherwise returns ``None``.
|
||||
"""
|
||||
if not (lookup_attrs := cls.EXPLORER_UI_LOOKUP_PROPS.get(ifc_class)):
|
||||
return
|
||||
return lookup_attrs
|
||||
@@ -61,6 +61,7 @@ if TYPE_CHECKING:
|
||||
from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties
|
||||
from bonsai.bim.module.csv.prop import CsvProperties
|
||||
from bonsai.bim.module.diff.prop import DiffProperties
|
||||
from bonsai.bim.module.fm.prop import BIMFMProperties
|
||||
from bonsai.bim.module.group.prop import BIMGroupProperties
|
||||
from bonsai.bim.module.light.prop import BIMSolarProperties, RadianceExporterProperties
|
||||
|
||||
@@ -785,7 +786,6 @@ class Blender(bonsai.core.tool.Blender):
|
||||
to False, which will make datablock deletion significantly faster
|
||||
by avoiding unnecessary Blender data checks.
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
collection_name = repr(data_block).split(".", 2)[-1].split("[", 1)[0]
|
||||
getattr(bpy.data, collection_name).remove(
|
||||
@@ -799,7 +799,6 @@ class Blender(bonsai.core.tool.Blender):
|
||||
:param data_blocks: iterable of data blocks to remove
|
||||
:param remove_unused_data: set to True to purge data that would be orphaned by the operation
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
data_blocks = list(data_blocks)
|
||||
if remove_unused_data:
|
||||
@@ -1535,6 +1534,67 @@ class Blender(bonsai.core.tool.Blender):
|
||||
obj.matrix_world = matrix
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_full_data_path(cls, bpy_struct: bpy.types.bpy_struct, path: str = "") -> str:
|
||||
"""Get full data path to Blender entity or it's attributes.
|
||||
|
||||
:param bpy_struct: Blender entity.
|
||||
:param path: Additional path to add to entity.
|
||||
|
||||
:return: Path in a format
|
||||
``bpy.data.scenes['Scene'].BIMExplorerProperties.entity_attributes[4].enum_value``
|
||||
"""
|
||||
if path:
|
||||
bpy_prop: bpy.types.bpy_prop # pyright: ignore[reportAttributeAccessIssue]
|
||||
bpy_prop = bpy_struct.path_resolve(path, False)
|
||||
return repr(bpy_prop)
|
||||
return repr(bpy_struct)
|
||||
|
||||
@classmethod
|
||||
def resolve_data_path_to_data_attr(cls, data_path: str) -> tuple[bpy.types.bpy_struct, str]:
|
||||
"""
|
||||
:param data_path: Non-full data path to attribute.
|
||||
Examples:
|
||||
- `preferences.prop_group.string_prop` (`preferences` would mean addon preferences)
|
||||
- `scene.string_prop` (`scene` can be any member of `Context`)
|
||||
|
||||
:return: Resolved tuple of Blender Struct and property name.
|
||||
Examples:
|
||||
- `(preferences.prop_group, "string_prop)`
|
||||
- `(scene, "string_prop)`
|
||||
|
||||
"""
|
||||
# Get data to modify.
|
||||
base_path, _, data_path_ = data_path.partition(".")
|
||||
if base_path == "preferences":
|
||||
data = tool.Blender.get_addon_preferences()
|
||||
data_path = data_path_
|
||||
else:
|
||||
data = bpy.context
|
||||
|
||||
# Get property group if available.
|
||||
base_path, _, attr = data_path.rpartition(".")
|
||||
if base_path:
|
||||
data = data.path_resolve(base_path)
|
||||
return data, attr
|
||||
|
||||
@classmethod
|
||||
@contextlib.contextmanager
|
||||
def preserve_prop_value(cls, bpy_object: bpy.types.bpy_struct, prop_name: str):
|
||||
if bpy_object.is_property_set(prop_name):
|
||||
prop_value = getattr(bpy_object, prop_name)
|
||||
else:
|
||||
prop_value = ...
|
||||
try:
|
||||
yield
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
if prop_value is ...:
|
||||
bpy_object.property_unset(prop_name)
|
||||
return
|
||||
setattr(bpy_object, prop_name, prop_value)
|
||||
|
||||
@classmethod
|
||||
def set_prop_from_path(cls, bpy_object: bpy.types.bpy_struct, prop_path: str, value: Any) -> None:
|
||||
"""Set `data_block` property value using path from `path_from_id`."""
|
||||
@@ -1603,7 +1663,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
@classmethod
|
||||
def get_user_data_dir(cls) -> Path:
|
||||
props = cls.get_bim_props()
|
||||
props = cls.get_addon_preferences()
|
||||
return Path(props.data_dir)
|
||||
|
||||
@classmethod
|
||||
@@ -1665,51 +1725,56 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
@classmethod
|
||||
def get_object_constraint_props(cls, obj: bpy.types.Object) -> BIMObjectConstraintProperties:
|
||||
return obj.BIMObjectConstraintProperties
|
||||
return obj.BIMObjectConstraintProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_constraint_props(cls) -> BIMConstraintProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMConstraintProperties
|
||||
return scene.BIMConstraintProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_csv_props(cls) -> CsvProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.CsvProperties
|
||||
return scene.CsvProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_diff_props(cls) -> DiffProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.DiffProperties
|
||||
return scene.DiffProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_group_props(cls) -> BIMGroupProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMGroupProperties
|
||||
return scene.BIMGroupProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties:
|
||||
if scene is None:
|
||||
scene = bpy.context.scene
|
||||
return scene.BIMProperties
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_object_bim_props(cls, obj: bpy.types.Object) -> BIMObjectProperties:
|
||||
return obj.BIMObjectProperties
|
||||
return obj.BIMObjectProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_object_attribute_props(cls, obj: bpy.types.Object) -> BIMAttributeProperties:
|
||||
return obj.BIMAttributeProperties
|
||||
return obj.BIMAttributeProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_solar_props(cls) -> BIMSolarProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMSolarProperties
|
||||
return scene.BIMSolarProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_radiance_exporter_props(cls) -> RadianceExporterProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMRadianceExporeterProperies
|
||||
return scene.BIMRadianceExporeterProperies # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_fm_props(cls) -> BIMFMProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMFMProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
|
||||
@@ -1845,3 +1910,59 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
# Remove file if crash didn't happened.
|
||||
path.unlink()
|
||||
|
||||
@classmethod
|
||||
def sync_old_preferences(cls) -> None:
|
||||
# Added on 25.07.15.
|
||||
# TODO: deprecate later.
|
||||
settings_remap = {
|
||||
"scene.BIMBSDDProperties.load_preview_dictionaries": "preferences.bsdd_load_preview_dictionaries",
|
||||
"scene.BIMBSDDProperties.load_inactive_dictionaries": "preferences.bsdd_load_inactive_dictionaries",
|
||||
"scene.BIMBSDDProperties.load_test_dictionaries": "preferences.bsdd_load_test_dictionaries",
|
||||
"scene.BIMProjectProperties.should_disable_undo_on_save": "preferences.should_disable_undo_on_save",
|
||||
"scene.BIMProjectProperties.should_stream": "preferences.should_stream",
|
||||
"scene.BIMModelProperties.occurrence_name_style": "preferences.occurrence_name_style",
|
||||
"scene.BIMModelProperties.occurrence_name_function": "preferences.occurrence_name_function",
|
||||
"scene.BIMProperties.pset_dir": "preferences.pset_dir",
|
||||
"scene.BIMProperties.data_dir": "preferences.data_dir",
|
||||
"scene.BIMProperties.cache_dir": "preferences.cache_dir",
|
||||
"scene.DocProperties.sheets_dir": "preferences.doc.sheets_dir",
|
||||
"scene.DocProperties.layouts_dir": "preferences.doc.layouts_dir",
|
||||
"scene.DocProperties.titleblocks_dir": "preferences.doc.titleblocks_dir",
|
||||
"scene.DocProperties.drawings_dir": "preferences.doc.drawings_dir",
|
||||
"scene.DocProperties.stylesheet_path": "preferences.doc.stylesheet_path",
|
||||
"scene.DocProperties.schedules_stylesheet_path": "preferences.doc.schedules_stylesheet_path",
|
||||
"scene.DocProperties.markers_path": "preferences.doc.markers_path",
|
||||
"scene.DocProperties.symbols_path": "preferences.doc.symbols_path",
|
||||
"scene.DocProperties.patterns_path": "preferences.doc.patterns_path",
|
||||
"scene.DocProperties.shadingstyles_path": "preferences.doc.shadingstyles_path",
|
||||
"scene.DocProperties.shadingstyle_default": "preferences.doc.shadingstyle_default",
|
||||
"scene.DocProperties.drawing_font": "preferences.doc.drawing_font",
|
||||
"scene.DocProperties.magic_font_scale": "preferences.doc.magic_font_scale",
|
||||
"scene.DocProperties.imperial_precision": "preferences.doc.imperial_precision",
|
||||
"scene.DocProperties.tolerance": "preferences.doc.tolerance",
|
||||
"scene.DocProperties.classes_to_wireframe": "preferences.doc.classes_to_wireframe",
|
||||
"scene.DocProperties.classes_no_cut": "preferences.doc.classes_no_cut",
|
||||
}
|
||||
|
||||
props_updated = False
|
||||
for old_path, path in settings_remap.items():
|
||||
data, attr = cls.resolve_data_path_to_data_attr(path)
|
||||
# User already overridden the value.
|
||||
if data.is_property_set(attr):
|
||||
continue
|
||||
|
||||
data_old, attr_old = cls.resolve_data_path_to_data_attr(old_path)
|
||||
# User was only using default value previously.
|
||||
if attr_old not in data_old:
|
||||
continue
|
||||
|
||||
old_value = data_old[attr_old]
|
||||
print(f"Updating {path} based on previous value from {old_path} - '{old_value}'.")
|
||||
setattr(data, attr, old_value)
|
||||
props_updated = True
|
||||
|
||||
# Doesn't seem to save on exit if edited from Python API, so we do it manually.
|
||||
assert bpy.context.preferences
|
||||
if props_updated and bpy.context.preferences.use_preferences_save:
|
||||
bpy.ops.wm.save_userpref()
|
||||
|
||||
@@ -107,20 +107,22 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
|
||||
@classmethod
|
||||
def get_dictionary(cls, uri: str) -> bsdd.DictionaryContractV1:
|
||||
props = cls.get_bsdd_props()
|
||||
response = cls.client.get_dictionary(dictionary_uri=uri, include_test_dictionaries=props.load_test_dictionaries)
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
response = cls.client.get_dictionary(
|
||||
dictionary_uri=uri, include_test_dictionaries=prefs.bsdd_load_test_dictionaries
|
||||
)
|
||||
if dicts := response.get("dictionaries"):
|
||||
return dicts[0]
|
||||
|
||||
@classmethod
|
||||
def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]:
|
||||
props = cls.get_bsdd_props()
|
||||
response = cls.client.get_dictionary(include_test_dictionaries=props.load_test_dictionaries)
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
response = cls.client.get_dictionary(include_test_dictionaries=prefs.bsdd_load_test_dictionaries)
|
||||
dicts = response.get("dictionaries") or []
|
||||
statuses = ["Active"]
|
||||
if props.load_preview_dictionaries:
|
||||
if prefs.bsdd_load_preview_dictionaries:
|
||||
statuses.append("Preview")
|
||||
if props.load_inactive_dictionaries:
|
||||
if prefs.bsdd_load_inactive_dictionaries:
|
||||
statuses.append("Inactive")
|
||||
return list(filter(lambda d: d["status"] in statuses, dicts))
|
||||
|
||||
|
||||
@@ -119,8 +119,7 @@ class Clash(bonsai.core.tool.Clash):
|
||||
if not clash_set:
|
||||
return
|
||||
clash_set.clashes.clear()
|
||||
if "clashes_loaded" in clash_set:
|
||||
del clash_set["clashes_loaded"]
|
||||
clash_set.property_unset("clashes_loaded")
|
||||
|
||||
@classmethod
|
||||
def load_clash_sets(cls, fn: str) -> None:
|
||||
|
||||
@@ -46,7 +46,7 @@ class Context(bonsai.core.tool.Context):
|
||||
props.context_attributes.clear()
|
||||
context = cls.get_context()
|
||||
|
||||
def callback(name: str, prop, data) -> Union[bool, None]:
|
||||
def callback(name: str, prop: Union[Attribute, None], data: dict[str, Any]) -> Union[bool, None]:
|
||||
if context.is_a("IfcGeometricRepresentationSubContext"):
|
||||
if name == "Precision":
|
||||
props.context_attributes.remove(props.context_attributes.find("Precision"))
|
||||
@@ -57,8 +57,9 @@ class Context(bonsai.core.tool.Context):
|
||||
elif name == "TargetScale":
|
||||
props.context_attributes.remove(props.context_attributes.find("TargetScale"))
|
||||
scale_denominator = None
|
||||
if data.get(name) is not None and data.get(name) != 0:
|
||||
scale_denominator = 1.0 / data.get(name)
|
||||
value = data.get(name)
|
||||
if value not in (None, 0):
|
||||
scale_denominator = 1.0 / value
|
||||
new_prop = props.context_attributes.add()
|
||||
new_prop.name = "ScaleDenominator"
|
||||
new_prop.data_type = "float"
|
||||
@@ -68,9 +69,10 @@ class Context(bonsai.core.tool.Context):
|
||||
else: # IfcGeometricRepresentationContext
|
||||
# Import precision as a string because Blender has problem displaying 1e-7 and smaller numbers in UI.
|
||||
if name == "Precision":
|
||||
assert prop
|
||||
prop.data_type = "string"
|
||||
|
||||
bonsai.bim.helper.import_attributes(context.is_a(), props.context_attributes, context.get_info(), callback)
|
||||
bonsai.bim.helper.import_attributes(context, props.context_attributes, callback)
|
||||
|
||||
@classmethod
|
||||
def clear_context(cls) -> None:
|
||||
|
||||
+107
-33
@@ -22,6 +22,7 @@ import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.cost
|
||||
import ifcopenshell.api.document
|
||||
import ifcopenshell.api.nest
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.date
|
||||
@@ -34,6 +35,7 @@ from typing import Optional, Any, Union, Literal, TYPE_CHECKING, assert_never
|
||||
from collections.abc import Generator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import Attribute
|
||||
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
|
||||
|
||||
|
||||
@@ -128,7 +130,7 @@ class Cost(bonsai.core.tool.Cost):
|
||||
|
||||
props = cls.get_cost_props()
|
||||
props.cost_schedule_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(cost_schedule, props.cost_schedule_attributes, callback=special_import)
|
||||
bonsai.bim.helper.import_attributes(cost_schedule, props.cost_schedule_attributes, callback=special_import)
|
||||
|
||||
@classmethod
|
||||
def enable_editing_cost_items(cls, cost_schedule: ifcopenshell.entity_instance) -> None:
|
||||
@@ -234,7 +236,7 @@ class Cost(bonsai.core.tool.Cost):
|
||||
def load_cost_item_attributes(cls, cost_item: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_cost_props()
|
||||
props.cost_item_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(cost_item, props.cost_item_attributes)
|
||||
bonsai.bim.helper.import_attributes(cost_item, props.cost_item_attributes)
|
||||
|
||||
@classmethod
|
||||
def disable_editing_cost_item(cls) -> None:
|
||||
@@ -379,7 +381,7 @@ class Cost(bonsai.core.tool.Cost):
|
||||
def load_cost_item_quantity_attributes(cls, physical_quantity: ifcopenshell.entity_instance) -> None:
|
||||
props = cls.get_cost_props()
|
||||
props.quantity_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(physical_quantity, props.quantity_attributes)
|
||||
bonsai.bim.helper.import_attributes(physical_quantity, props.quantity_attributes)
|
||||
|
||||
@classmethod
|
||||
def enable_editing_cost_item_values(cls, cost_item: ifcopenshell.entity_instance) -> None:
|
||||
@@ -415,7 +417,13 @@ class Cost(bonsai.core.tool.Cost):
|
||||
|
||||
@classmethod
|
||||
def load_cost_item_value_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None:
|
||||
def import_attributes(name, prop, data, cost_value, is_rates, props_collection):
|
||||
props = cls.get_cost_props()
|
||||
props.cost_value_attributes.clear()
|
||||
props_collection = props.cost_value_attributes
|
||||
# is_rates = cls.is_active_schedule_of_rates()
|
||||
is_rates = True # so it is possible to assign a cost item rate that it not only from a Schedule of Rate
|
||||
|
||||
def import_attributes_callback(name: str, prop: Union[Attribute, None], data) -> None | Literal[True]:
|
||||
if name == "AppliedValue":
|
||||
# TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure)
|
||||
prop = props_collection.add()
|
||||
@@ -452,14 +460,9 @@ class Cost(bonsai.core.tool.Cost):
|
||||
break
|
||||
return True
|
||||
|
||||
props = cls.get_cost_props()
|
||||
props.cost_value_attributes.clear()
|
||||
# is_rates = cls.is_active_schedule_of_rates()
|
||||
is_rates = True # so it is possible to assign a cost item rate that it not only from a Schedule of Rate
|
||||
callback = lambda name, prop, data: import_attributes(
|
||||
name, prop, data, cost_value, is_rates, props.cost_value_attributes
|
||||
bonsai.bim.helper.import_attributes(
|
||||
cost_value, props.cost_value_attributes, callback=import_attributes_callback
|
||||
)
|
||||
bonsai.bim.helper.import_attributes2(cost_value, props.cost_value_attributes, callback=callback)
|
||||
|
||||
@classmethod
|
||||
def calculate_applied_value(
|
||||
@@ -584,11 +587,33 @@ class Cost(bonsai.core.tool.Cost):
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
csv2ifc = Csv2Ifc(file_path, tool.Ifc.get(), is_schedule_of_rates=is_schedule_of_rates)
|
||||
|
||||
resolved_path = tool.Ifc.resolve_uri(file_path)
|
||||
|
||||
csv2ifc = Csv2Ifc(resolved_path, tool.Ifc.get(), is_schedule_of_rates=is_schedule_of_rates)
|
||||
csv2ifc.execute()
|
||||
print("Import finished in {:.2f} seconds".format(time.time() - start))
|
||||
return csv2ifc.cost_schedule
|
||||
|
||||
@classmethod
|
||||
def get_or_create_cost_documents(cls) -> ifcopenshell.entity_instance:
|
||||
ifc_file = tool.Ifc.get()
|
||||
cost_docs_document = next(
|
||||
(
|
||||
document
|
||||
for document in ifc_file.by_type("IfcDocumentInformation")
|
||||
if document.Name == "BBIM_Cost_Documents"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not cost_docs_document:
|
||||
cost_docs_document = ifcopenshell.api.document.add_information(ifc_file)
|
||||
cost_docs_document.Name = "BBIM_Cost_Documents"
|
||||
cost_docs_document.Description = "Bonsai internal document containing references to cost CSV files"
|
||||
|
||||
return cost_docs_document
|
||||
|
||||
@classmethod
|
||||
def add_csv_filepath(
|
||||
cls,
|
||||
@@ -596,33 +621,41 @@ class Cost(bonsai.core.tool.Cost):
|
||||
is_schedule_of_rates: bool = False,
|
||||
cost_schedule: ifcopenshell.entity_instance = None,
|
||||
) -> None:
|
||||
if not file_path:
|
||||
if not file_path or not cost_schedule:
|
||||
return
|
||||
|
||||
props = cls.get_cost_props()
|
||||
if not props.active_cost_schedule_id in [item.cost_schedule_id for item in props.cost_schedule_files]:
|
||||
item = props.cost_schedule_files.add()
|
||||
item.cost_schedule_id = cost_schedule.id()
|
||||
item.csv_filepath = file_path
|
||||
ifc_file = tool.Ifc.get()
|
||||
cost_docs_document = cls.get_or_create_cost_documents()
|
||||
|
||||
reference = ifcopenshell.api.document.add_reference(ifc_file, cost_docs_document)
|
||||
reference.Location = file_path
|
||||
|
||||
reference.Description = f"Cost Schedule ID: {cost_schedule.id()}"
|
||||
|
||||
if is_schedule_of_rates:
|
||||
reference.Identification = "SCHEDULE_OF_RATES"
|
||||
else:
|
||||
return
|
||||
reference.Identification = "COST_SCHEDULE"
|
||||
|
||||
@classmethod
|
||||
def remove_csv_filepath(cls, cost_schedule: ifcopenshell.entity_instance = None) -> None:
|
||||
if not cost_schedule:
|
||||
return
|
||||
|
||||
props = cls.get_cost_props()
|
||||
cost_schedule_id = cost_schedule.id()
|
||||
if cost_schedule_id in [item.cost_schedule_id for item in props.cost_schedule_files]:
|
||||
for i, item in enumerate(props.cost_schedule_files):
|
||||
if item.cost_schedule_id == cost_schedule_id:
|
||||
props.cost_schedule_files.remove(i)
|
||||
print(f"Cost schedule id={cost_schedule_id} csv filepath correctly removed")
|
||||
return
|
||||
else:
|
||||
ifc_file = tool.Ifc.get()
|
||||
cost_docs_document = cls.get_or_create_cost_documents()
|
||||
|
||||
if not cost_docs_document:
|
||||
return
|
||||
|
||||
cost_schedule_id = cost_schedule.id()
|
||||
references = tool.Document.get_document_references(cost_docs_document)
|
||||
|
||||
for reference in references:
|
||||
if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description:
|
||||
ifcopenshell.api.document.remove_reference(ifc_file, reference)
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def delete_all_cost_items(cls):
|
||||
cost_schedule = tool.Cost.get_active_cost_schedule()
|
||||
@@ -632,25 +665,59 @@ class Cost(bonsai.core.tool.Cost):
|
||||
ifcopenshell.api.cost.remove_cost_item(tool.Ifc.get(), cost_item=item)
|
||||
tool.Cost.clean_up_cost_item_tree(cost_item_id)
|
||||
|
||||
@classmethod
|
||||
def is_schedule_of_rates_csv(cls, cost_schedule_id: int) -> bool:
|
||||
"""Check if a cost schedule is a schedule of rates based on document references."""
|
||||
cost_docs_document = cls.get_or_create_cost_documents()
|
||||
|
||||
if not cost_docs_document:
|
||||
return False
|
||||
|
||||
references = tool.Document.get_document_references(cost_docs_document)
|
||||
|
||||
for reference in references:
|
||||
if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description:
|
||||
return reference.Identification == "SCHEDULE_OF_RATES"
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_cost_schedule_csv_filepath(cls, cost_schedule_id: int) -> Optional[str]:
|
||||
cost_docs_document = cls.get_or_create_cost_documents()
|
||||
|
||||
if not cost_docs_document:
|
||||
return None
|
||||
|
||||
references = tool.Document.get_document_references(cost_docs_document)
|
||||
|
||||
for reference in references:
|
||||
if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description:
|
||||
return reference.Location
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def refresh_cost_schedule_csv(cls):
|
||||
"""Refresh cost schedule from CSV file stored in document references."""
|
||||
from ifc5d.csv2ifc import Csv2Ifc
|
||||
|
||||
props = cls.get_cost_props()
|
||||
cost_schedule_id = props.active_cost_schedule_id
|
||||
file_path = next(
|
||||
(item.csv_filepath for item in props.cost_schedule_files if item.cost_schedule_id == cost_schedule_id), None
|
||||
)
|
||||
file_path = cls.get_cost_schedule_csv_filepath(cost_schedule_id)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
resolved_path = tool.Ifc.resolve_uri(file_path)
|
||||
|
||||
cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id)
|
||||
is_schedule_of_rates = cls.is_schedule_of_rates_csv(cost_schedule_id)
|
||||
|
||||
csv2ifc = Csv2Ifc()
|
||||
csv2ifc.csv = file_path
|
||||
csv2ifc.csv = resolved_path
|
||||
csv2ifc.file = tool.Ifc.get()
|
||||
csv2ifc.cost_schedule = cost_schedule
|
||||
csv2ifc.is_schedule_of_rates = False
|
||||
csv2ifc.is_schedule_of_rates = is_schedule_of_rates
|
||||
csv2ifc.refresh()
|
||||
|
||||
print("Csv file correctly refreshed")
|
||||
@@ -778,6 +845,13 @@ class Cost(bonsai.core.tool.Cost):
|
||||
except:
|
||||
return "Could not open file location"
|
||||
|
||||
@classmethod
|
||||
def export_cost_schedules_to_pdf(cls, filepath: str, cost_schedule: ifcopenshell.entity_instance, options: dict):
|
||||
from ifc5d.ifc5Dspreadsheet import Ifc5DPdfWriter
|
||||
|
||||
writer = Ifc5DPdfWriter(file=tool.Ifc.get(), output=filepath, cost_schedule=cost_schedule, options=options)
|
||||
writer.write()
|
||||
|
||||
@classmethod
|
||||
def get_units(cls) -> dict[int, str]:
|
||||
units = {}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import json
|
||||
import bmesh
|
||||
import bpy
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.api.material
|
||||
@@ -56,8 +57,8 @@ class Debug(bonsai.core.tool.Debug):
|
||||
|
||||
@classmethod
|
||||
def purge_hdf5_cache(cls) -> None:
|
||||
props = tool.Blender.get_bim_props()
|
||||
cache_dir = props.cache_dir
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
cache_dir = prefs.cache_dir
|
||||
filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")]
|
||||
for f in filelist:
|
||||
try:
|
||||
@@ -70,6 +71,7 @@ class Debug(bonsai.core.tool.Debug):
|
||||
mesh = bpy.data.meshes.new("Debug")
|
||||
bm.to_mesh(mesh)
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
assert bpy.context.scene
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@@ -80,6 +82,7 @@ class Debug(bonsai.core.tool.Debug):
|
||||
mesh = bpy.data.meshes.new("Debug")
|
||||
mesh.from_pydata(verts, edges, [])
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
assert bpy.context.scene
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user